123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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(/<br\/>/g, '\n')
- .replace(/<br \/>/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)} \n\n ${status.uri}`))
- }
- })
- this.lastPostDate = lastPostDateRecord
- })
- }, 10000)
- }
- }
- module.exports = bot
|