XTweet/bot.js
2019-08-06 09:40:28 +02:00

206 lines
6.7 KiB
JavaScript

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(/&apos;/g, '\'')
.replace(/&quot;/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