bot.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. let Mastodon = require('mastodon')
  2. let Twit = require('twit')
  3. // for downloading and saving media
  4. const https = require('https')
  5. const fs = require('fs')
  6. const mime = require('mime-types')
  7. // load the configuration
  8. const config = require('./config.js')
  9. let storage = require('./storage.js')
  10. const TWITTER_MAX_CHARS = 280
  11. const TWITTER_IMAGE_MAX_CHARS = 140 //?
  12. function readBase64(filename) {
  13. let buf = fs.readFileSync(filename)
  14. let b64 = buf.toString('base64')
  15. return b64
  16. }
  17. let bot = {
  18. lastPostDate: new Date().getTime(),
  19. // splits a tweet into pieces that are less than TWITTER_MAX_CHARS long
  20. splitTweet: function (post, pieces = []) {
  21. if (post.length > TWITTER_MAX_CHARS) {
  22. let index = TWITTER_MAX_CHARS - 3 // 3 for appending ...
  23. while (post.charAt(index) !== ' ' && index > 0) index--
  24. if (index === 0) {
  25. /*if a word if longer than TWITTER_MAX_CHARS-3 split the word*/
  26. index = TWITTER_MAX_CHARS - 3
  27. }
  28. return this.splitTweet(post.substring(index+1), pieces.concat([post.substring(0, index) + '...']))
  29. }
  30. return pieces.concat([post])
  31. },
  32. splitForImage: function (post) {
  33. if (post.length > TWITTER_IMAGE_MAX_CHARS) {
  34. let index = TWITTER_IMAGE_MAX_CHARS - 3 // 3 for appending ...
  35. while (post.charAt(index) !== ' ' && index > 0) index--
  36. if (index === 0) {
  37. /*if a word if longer than TWITTER_MAX_CHARS-3 split the word*/
  38. index = TWITTER_IMAGE_MAX_CHARS - 3
  39. }
  40. return [post.substring(0, index) + '...', post.substring(index+1)]
  41. }
  42. return [post]
  43. },
  44. // post to twitter with media attachments
  45. // Size restrictions for uploading via API
  46. // Image 5MB
  47. // GIF 15MB
  48. // Video 15MB
  49. // medias is an array of filenames
  50. twitterPostWithMedias: function (post, medias) {
  51. let that = this
  52. let pieces = this.splitForImage(post)
  53. let uploads = []
  54. for (let media of medias) {
  55. let b64content = readBase64(media)
  56. uploads.push(
  57. new Promise((resolve, reject) => {
  58. that.twitter.post('media/upload', { media_data: b64content }, (err, data, response) => {
  59. if (err) reject(err)
  60. else resolve(data)
  61. })
  62. })
  63. )
  64. }
  65. return Promise.all(uploads).then(datas => {
  66. let mediaIds = datas.map(data => data.media_id_string)
  67. return new Promise((resolve, reject) => {
  68. that.twitter.post('statuses/update', { status: pieces[0], media_ids: mediaIds }, (err, data, response) => {
  69. if (!err) {
  70. resolve(data)
  71. } else reject(err)
  72. })
  73. })
  74. }).then((data) => {
  75. if (pieces.length > 1) return that.twitterPost(pieces[1], data.id_str)
  76. else return Promise.resolve()
  77. }).then(() => {
  78. console.log(`[INFO] Tweeted ${post} with ${medias}`)
  79. })
  80. },
  81. promisePost: function(post, reply = null) {
  82. let that = this
  83. return new Promise((resolve, reject) => {
  84. that.twitter.post('statuses/update', { status: post, in_reply_to_status_id: reply }, (err, data, response) => {
  85. if (err) reject(err)
  86. else resolve(data)
  87. })
  88. })
  89. },
  90. // post to twitter
  91. // if post is longer than TWITTER_MAX_CHARS divides in multiple tweets
  92. twitterPost: function (post, reply = null) {
  93. let pieces = this.splitTweet(post)
  94. let promise = this.promisePost(pieces[0], reply)
  95. for (let i = 1; i < pieces.length; i++) {
  96. promise = promise.then(data => this.promisePost(pieces[i], data.id_str))
  97. }
  98. return promise
  99. .catch(err => console.error(err))
  100. },
  101. // downloads a file from the given url and saves it to temp dir
  102. downloadFile: function(url, name, temp = '/tmp') {
  103. return new Promise(resolve => {
  104. https.get(url, response => {
  105. let ext = mime.extension(response.headers['content-type'])
  106. let filename = `${temp}/${name}.${ext}`
  107. const file = fs.createWriteStream(filename)
  108. const stream = response.pipe(file)
  109. stream.on('finish', () => {
  110. console.log(`[INFO] Downloaded ${url} to ${filename}`)
  111. resolve(filename)
  112. })
  113. })
  114. })
  115. },
  116. // makes a post formatted for twitter
  117. // mastodon returns posts in HTML
  118. twitterFormat: function(post) {
  119. return post
  120. .replace(/<br\/>/g, '\n')
  121. .replace(/<br \/>/g, '\n')
  122. .replace(/<\/p>/g, '\n')
  123. .replace(/&apos;/g, '\'')
  124. .replace(/&quot;/g, '"')
  125. .replace(/<[^>]*>?/gm, '')
  126. },
  127. // poll on mastodon status updates
  128. start: function() {
  129. this.twitter = new Twit({
  130. consumer_key: config.twitter_consumer_key,
  131. consumer_secret: config.twitter_consumer_secret,
  132. access_token: storage.data.token,
  133. access_token_secret: storage.data.token_secret,
  134. timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests
  135. strictSSL: true, // optional - requires SSL certificates to be valid
  136. })
  137. this.mastodon = new Mastodon({
  138. access_token: config.mastodon_token,
  139. timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests.
  140. api_url: `https://${config.instance}/api/v1/`, // optional, defaults to https://mastodon.social/api/v1/
  141. })
  142. setInterval(() => {
  143. this.mastodon.get('timelines/home', {}).then(resp => {
  144. let lastPostDateRecord = this.lastPostDate
  145. resp.data
  146. .filter(it => it.account.username === config.mastodon_user)
  147. .filter(it => Date.parse(it.created_at) > this.lastPostDate)
  148. .forEach((status) => {
  149. let date = Date.parse(status.created_at)
  150. if (date > lastPostDateRecord) lastPostDateRecord = date
  151. if (status.media_attachments.length > 0) {
  152. let downloads = status.media_attachments.map(media =>
  153. this.downloadFile(media.url, media.id)
  154. )
  155. Promise.all(downloads)
  156. .then(files => {
  157. this.twitterPostWithMedias(`${this.twitterFormat(status.content)} \n\n ${status.uri}`, files)
  158. .then(() => {
  159. files.forEach(file => {
  160. fs.unlink(file, (err) => {
  161. if (err) throw err
  162. console.log(`[INFO] Deleted ${file}`)
  163. })
  164. })
  165. }).catch(e => { console.error(e) })
  166. }).catch(err => { console.error(err) })
  167. } else {
  168. this.twitterPost(`${this.twitterFormat(status.content)} \n\n ${status.uri}`)
  169. .then(() => console.log(`[INFO] Tweeted ${this.twitterFormat(status.content)} \n\n ${status.uri}`))
  170. }
  171. })
  172. this.lastPostDate = lastPostDateRecord
  173. })
  174. }, 10000)
  175. }
  176. }
  177. module.exports = bot