let Mastodon = require('mastodon') let Twit = require('twit') // for downloading and saving media const https = require('https') const fs = require('fs') const mime = require('mime-types') // load the configuration const config = require('./config.js') let storage = require('./storage.js') const TWITTER_MAX_CHARS = 280 const TWITTER_IMAGE_MAX_CHARS = 140 //? function readBase64(filename) { let buf = fs.readFileSync(filename) let b64 = buf.toString('base64') return b64 } let bot = { lastPostDate: new Date().getTime(), // splits a tweet into pieces that are less than TWITTER_MAX_CHARS long splitTweet: function (post, pieces = []) { if (post.length > TWITTER_MAX_CHARS) { let index = TWITTER_MAX_CHARS - 3 // 3 for appending ... while (post.charAt(index) !== ' ' && index > 0) index-- if (index === 0) { /*if a word if longer than TWITTER_MAX_CHARS-3 split the word*/ index = TWITTER_MAX_CHARS - 3 } return this.splitTweet(post.substring(index+1), pieces.concat([post.substring(0, index) + '...'])) } return pieces.concat([post]) }, splitForImage: function (post) { if (post.length > TWITTER_IMAGE_MAX_CHARS) { let index = TWITTER_IMAGE_MAX_CHARS - 3 // 3 for appending ... while (post.charAt(index) !== ' ' && index > 0) index-- if (index === 0) { /*if a word if longer than TWITTER_MAX_CHARS-3 split the word*/ index = TWITTER_IMAGE_MAX_CHARS - 3 } return [post.substring(0, index) + '...', post.substring(index+1)] } return [post] }, // post to twitter with media attachments // Size restrictions for uploading via API // Image 5MB // GIF 15MB // Video 15MB // medias is an array of filenames twitterPostWithMedias: function (post, medias) { let that = this let pieces = this.splitForImage(post) let uploads = [] for (let media of medias) { let b64content = readBase64(media) uploads.push( new Promise((resolve, reject) => { that.twitter.post('media/upload', { media_data: b64content }, (err, data, response) => { if (err) reject(err) else resolve(data) }) }) ) } return Promise.all(uploads).then(datas => { let mediaIds = datas.map(data => data.media_id_string) return new Promise((resolve, reject) => { that.twitter.post('statuses/update', { status: pieces[0], media_ids: mediaIds }, (err, data, response) => { if (!err) { resolve(data) } else reject(err) }) }) }).then((data) => { if (pieces.length > 1) return that.twitterPost(pieces[1], data.id_str) else return Promise.resolve() }).then(() => { console.log(`[INFO] Tweeted ${post} with ${medias}`) }) }, promisePost: function(post, reply = null) { let that = this return new Promise((resolve, reject) => { that.twitter.post('statuses/update', { status: post, in_reply_to_status_id: reply }, (err, data, response) => { if (err) reject(err) else resolve(data) }) }) }, // post to twitter // if post is longer than TWITTER_MAX_CHARS divides in multiple tweets twitterPost: function (post, reply = null) { let pieces = this.splitTweet(post) let promise = this.promisePost(pieces[0], reply) for (let i = 1; i < pieces.length; i++) { promise = promise.then(data => this.promisePost(pieces[i], data.id_str)) } return promise .catch(err => console.error(err)) }, // downloads a file from the given url and saves it to temp dir downloadFile: function(url, name, temp = '/tmp') { return new Promise(resolve => { https.get(url, response => { let ext = mime.extension(response.headers['content-type']) let filename = `${temp}/${name}.${ext}` const file = fs.createWriteStream(filename) const stream = response.pipe(file) stream.on('finish', () => { console.log(`[INFO] Downloaded ${url} to ${filename}`) resolve(filename) }) }) }) }, // makes a post formatted for twitter // mastodon returns posts in HTML twitterFormat: function(post) { return post .replace(//g, '\n') .replace(/
/g, '\n') .replace(/<\/p>/g, '\n') .replace(/'/g, '\'') .replace(/"/g, '"') .replace(/<[^>]*>?/gm, '') }, // poll on mastodon status updates start: function() { this.twitter = new Twit({ consumer_key: config.twitter_consumer_key, consumer_secret: config.twitter_consumer_secret, access_token: storage.data.token, access_token_secret: storage.data.token_secret, timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests strictSSL: true, // optional - requires SSL certificates to be valid }) this.mastodon = new Mastodon({ access_token: config.mastodon_token, timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests. api_url: `https://${config.instance}/api/v1/`, // optional, defaults to https://mastodon.social/api/v1/ }) setInterval(() => { this.mastodon.get('timelines/home', {}).then(resp => { let lastPostDateRecord = this.lastPostDate resp.data .filter(it => it.account.username === config.mastodon_user) .filter(it => Date.parse(it.created_at) > this.lastPostDate) .forEach((status) => { let date = Date.parse(status.created_at) if (date > lastPostDateRecord) lastPostDateRecord = date if (status.media_attachments.length > 0) { let downloads = status.media_attachments.map(media => this.downloadFile(media.url, media.id) ) Promise.all(downloads) .then(files => { this.twitterPostWithMedias(`${this.twitterFormat(status.content)} \n\n ${status.uri}`, files) .then(() => { files.forEach(file => { fs.unlink(file, (err) => { if (err) throw err console.log(`[INFO] Deleted ${file}`) }) }) }).catch(e => { console.error(e) }) }).catch(err => { console.error(err) }) } else { this.twitterPost(`${this.twitterFormat(status.content)} \n\n ${status.uri}`) .then(() => console.log(`[INFO] Tweeted ${this.twitterFormat(status.content)}`)) } }) this.lastPostDate = lastPostDateRecord }) }, 10000) } } module.exports = bot