diff --git a/README.md b/README.md index 4ff5c772..e97f7293 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ in the appropriate channel's topic to use it. ## Commands -Total: 443 +Total: 444 ### Utility: @@ -560,6 +560,7 @@ Total: 443 * **square:** Draws an image or a user's avatar as a square. * **squish:** Draws an image or a user's avatar but squished across the X or Y axis. * **tint:** Draws an image or a user's avatar but tinted a specific color. +* **tweet:** Sends a Twitter tweet with the user and text of your choice. * **zero-dialogue:** Sends a text box from Megaman Zero with the quote of your choice. ### Avatar Manipulation: @@ -1350,6 +1351,7 @@ here. - [Turning Point USA](https://www.tpusa.com/) * dear-liberals (Image) - [Twitter](https://twitter.com/) + * tweet ([Image, API](https://developer.twitter.com/en/docs.html)) * twitter ([API](https://developer.twitter.com/en/docs.html)) - [u/_Ebb](https://www.reddit.com/user/_Ebb) * eat-pant ([Image](https://www.reddit.com/r/Ooer/comments/52z589/eat_pant_maaaaaaaan/)) diff --git a/assets/fonts/Noto-Bold.ttf b/assets/fonts/Noto-Bold.ttf new file mode 100644 index 00000000..ab11d316 Binary files /dev/null and b/assets/fonts/Noto-Bold.ttf differ diff --git a/assets/images/tweet.png b/assets/images/tweet.png new file mode 100644 index 00000000..73e480d6 Binary files /dev/null and b/assets/images/tweet.png differ diff --git a/commands/edit-image/achievement.js b/commands/edit-image/achievement.js index 17d4343b..7c08d2f6 100644 --- a/commands/edit-image/achievement.js +++ b/commands/edit-image/achievement.js @@ -60,4 +60,3 @@ module.exports = class AchievementCommand extends Command { return msg.say({ files: [{ attachment: canvas.toBuffer(), name: 'achievement.png' }] }); } }; - diff --git a/commands/edit-image/tweet.js b/commands/edit-image/tweet.js new file mode 100644 index 00000000..ce31654a --- /dev/null +++ b/commands/edit-image/tweet.js @@ -0,0 +1,149 @@ +const Command = require('../../structures/Command'); +const { createCanvas, loadImage, registerFont } = require('canvas'); +const moment = require('moment'); +const request = require('node-superfetch'); +const path = require('path'); +const { base64 } = require('../../util/Util'); +const { wrapText } = require('../../util/Canvas'); +const { TWITTER_KEY, TWITTER_SECRET } = process.env; +registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-Regular.ttf'), { family: 'Noto' }); +registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-Bold.ttf'), { family: 'Noto', weight: 'bold' }); +registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-CJK.otf'), { family: 'Noto' }); +registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-Emoji.ttf'), { family: 'Noto' }); + +module.exports = class TweetCommand extends Command { + constructor(client) { + super(client, { + name: 'tweet', + aliases: ['fake-tweet', 'twitter-tweet', 'fake-twitter-tweet'], + group: 'edit-image', + memberName: 'tweet', + description: 'Sends a Twitter tweet with the user and text of your choice.', + throttling: { + usages: 1, + duration: 10 + }, + clientPermissions: ['ATTACH_FILES'], + credit: [ + { + name: 'Twitter', + url: 'https://twitter.com/', + reason: 'Image, API', + reasonURL: 'https://developer.twitter.com/en/docs.html' + } + ], + args: [ + { + key: 'user', + prompt: 'What user should say the tweet? Use the handle, not the name.', + type: 'string', + max: 15 + }, + { + key: 'text', + prompt: 'What should the text of the achievement be?', + type: 'string', + max: 280 + } + ] + }); + + this.token = null; + } + + async run(msg, { user, text }) { + try { + if (!this.token) await this.fetchToken(); + const userData = await this.fetchUser(msg, user); + const avatar = await loadImage(userData.avatar); + const base = await loadImage(path.join(__dirname, '..', '..', 'assets', 'images', 'tweet.png')); + const canvas = createCanvas(base.width, base.height); + const ctx = canvas.getContext('2d'); + const likes = Math.floor(Math.random() * 999999) + 1; + const retweets = Math.floor(Math.random() * 999999) + 1; + const replies = Math.floor(Math.random() * 999999) + 1; + ctx.drawImage(base, 0, 0); + ctx.beginPath(); + ctx.arc(30, 84, 32, 0, Math.PI * 2); + ctx.closePath(); + ctx.clip(); + ctx.drawImage(avatar, 30, 84, 64, 64); + ctx.textBaseline = 'top'; + ctx.font = 'normal bold 18px Noto'; + ctx.fillStyle = 'white'; + ctx.fillText(userData.name, 105, 95); + ctx.font = '17px Noto'; + ctx.fillStyle = '#8899a6'; + ctx.fillText(userData.screenName, 106, 118); + ctx.fillStyle = 'white'; + ctx.font = '23px Noto'; + const lines = await wrapText(ctx, text, 710); + ctx.fillText(lines.join('\n'), 32, 171); + ctx.fillStyle = '#8899a6'; + ctx.font = '18px Noto'; + ctx.fillText(moment().format('h:mm A ∙ MMMM D, YYYY'), 31, 282); + ctx.font = '16px Noto'; + ctx.fillText(this.formatNumber(replies), 87, 470); + ctx.fillText(this.formatNumber(likes), 509, 470); + ctx.fillText(this.formatNumber(retweets), 300, 470); + ctx.fillStyle = 'white'; + ctx.font = 'normal bold 18px Noto'; + ctx.fillText(this.formatNumber(retweets), 31, 407); + const retweetsLen = ctx.measureText(this.formatNumber(retweets)); + ctx.fillStyle = '#8899a6'; + ctx.font = '18px Noto'; + ctx.fillText('Retweets', 31 + retweetsLen + 5, 407); + const retweetsWordLen = ctx.measureText('Retweets'); + ctx.fillStyle = 'white'; + ctx.font = 'normal bold 18px Noto'; + ctx.fillText(this.formatNumber(likes), 31 + retweetsLen + 5 + retweetsWordLen + 10, 407); + const likesLen = ctx.measureText(this.formatNumber(likes)); + ctx.fillStyle = '#8899a6'; + ctx.font = '18px Noto'; + ctx.fillText('Likes', 31 + retweetsLen + 5 + retweetsWordLen + 10 + likesLen + 5, 407); + return msg.say({ files: [{ attachment: canvas.toBuffer(), name: 'tweet.png' }] }); + } catch (err) { + return msg.reply(`Oh no, an error occurred: \`${err.message}\`. Try again later!`); + } + } + + async fetchToken() { + const { body } = await request + .post('https://api.twitter.com/oauth2/token') + .set({ + Authorization: `Basic ${base64(`${TWITTER_KEY}:${TWITTER_SECRET}`)}`, + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }) + .send('grant_type=client_credentials'); + this.token = body.access_token; + return body; + } + + async fetchUser(msg, user) { + try { + const { body } = await request + .get('https://api.twitter.com/1.1/users/show.json') + .set({ Authorization: `Bearer ${this.token}` }) + .query({ screen_name: user }); + const avatarRes = await request.get(body.profile_image_url_https); + return { + screenName: body.screen_name, + name: body.name, + avatar: avatarRes.body, + verified: body.verified + }; + } catch { + const avatarRes = await request.get(msg.author.displayAvatarURL({ format: 'png', size: 64 })); + return { + screenName: msg.author.username.slice(0, 15), + name: msg.member ? msg.member.displayName.slice(0, 50) : msg.author.username.slice(0, 50), + avatar: avatarRes.body, + verified: false + }; + } + } + + formatNumber(number) { + return number > 999 ? `${(number / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}K` : number; + } +}; diff --git a/package.json b/package.json index 9f12aa91..5ff47d16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "114.28.2", + "version": "114.29.0", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": {