diff --git a/.gitignore b/.gitignore index 5adb5f4..f8b697e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules config.js +storage.json diff --git a/app.js b/app.js new file mode 100644 index 0000000..99a42cb --- /dev/null +++ b/app.js @@ -0,0 +1,26 @@ + +let oauth = require('./oauth.js') +let storage = require('./storage.js') +let bot = require('./bot.js') + +const crypto = require('crypto') + + +// main +let main = () => { + storage.init() + + if (!storage.data.sess_secret) { + storage.data.sess_secret = crypto.randomBytes(128).toString('base64') + storage.save() + } + + if (storage.data.token && storage.data.token_secret) { + console.log("[INFO] Starting bot") + bot.start() + } else { + oauth.start() + } +} + +main() diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..76db3a9 --- /dev/null +++ b/bot.js @@ -0,0 +1,197 @@ + +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 pieces = this.splitForImage(post) + + let uploads = [] + + for (let media of medias) { + let b64content = readBase64(media) + uploads.push( + new Promise((resolve, reject) => { + this.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) + let altText = 'test' + mediaIds.forEach(mediaId => { + this.twitter.post('media/metadata/create', { media_id: mediaId, alt_text: { text: altText } }, (err, data, response) => { + if (!err) { + this.twitter.post('statuses/update', { status: pieces[0], media_ids: mediaIds }, (err, data, response) => { + if (!err) { + console.log(`[INFO] Tweeted ${post} with ${medias}`) + if (pieces.length > 1) this.twitterPost(pieces[1], data.id_str) + } else throw err + }) + } else throw err + }) + }) + }) + }, + + // post to twitter + // if post is longer than TWITTER_MAX_CHARS divides in multiple tweets + twitterPost: function (post, reply = null) { + let replyTo = reply + let success = true + this.splitTweet(post).forEach((piece) => { + this.twitter.post('statuses/update', { status: piece, in_reply_to_status_id: replyTo }, (err, data, response) => { + if (err) { + success = false + console.error(err) + } else { + replyTo = data.id_str + } + }) + }) + + if (success) console.log(`[INFO] Tweeted ${post}`) + }, + + // 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) { + post = post.replace('
', '\n') // new lines + post = post.replace('

', '') + post = post.replace('

', '\n') + return post + }, + + // 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), 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)) + } + }) + + this.lastPostDate = lastPostDateRecord + + }) + + }, 10000) + } +} + +module.exports = bot diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..c4852b7 --- /dev/null +++ b/cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +Pvn5BPPFdg== +-----END CERTIFICATE----- diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..a76b501 --- /dev/null +++ b/key.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +eMuOya/vdpCeoLRwdxXAPg== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/main.js b/main.js deleted file mode 100644 index ed1a4a0..0000000 --- a/main.js +++ /dev/null @@ -1,189 +0,0 @@ - -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') - -const TWITTER_MAX_CHARS = 280 -const TWITTER_IMAGE_MAX_CHARS = 140 //? - -let twitter = new Twit({ - consumer_key: config.twitter_consumer_key, - consumer_secret: config.twitter_consumer_secret, - access_token: config.twitter_access_token, - access_token_secret: config.twitter_access_token_secret, - timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests - strictSSL: true, // optional - requires SSL certificates to be valid -}) - -let 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/ -}) - -let lastPostDate = new Date().getTime() - -// splits a tweet into pieces that are less than TWITTER_MAX_CHARS long -function splitTweet(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 splitTweet(post.substring(index+1), pieces.concat([post.substring(0, index) + '...'])) - } - return pieces.concat([post]) -} - -function splitForImage(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] -} - -function readBase64(filename) { - let buf = fs.readFileSync(filename) - let b64 = buf.toString('base64') - return b64 -} - -// post to twitter with media attachments -// Size restrictions for uploading via API -// Image 5MB -// GIF 15MB -// Video 15MB -// medias is an array of filenames -function twitterPostWithMedias(post, medias) { - let pieces = splitForImage(twitterFormat(post)) - - let uploads = [] - - for (let media of medias) { - let b64content = readBase64(media) - uploads.push( - new Promise((resolve, reject) => { - 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) - let altText = 'test' - mediaIds.forEach(mediaId => { - twitter.post('media/metadata/create', { media_id: mediaId, alt_text: { text: altText } }, (err, data, response) => { - if (!err) { - twitter.post('statuses/update', { status: pieces[0], media_ids: mediaIds }, (err, data, response) => { - if (!err) { - console.log(`[INFO] Tweeted ${post} with ${medias}`) - if (pieces.length > 1) twitterPost(pieces[1], data.id_str) - } else throw err - }) - } else throw err - }) - }) - }) -} - -// post to twitter -// if post is longer than TWITTER_MAX_CHARS divides in multiple tweets -function twitterPost(post, reply = null) { - let replyTo = reply - let success = true - splitTweet(post).forEach((piece) => { - twitter.post('statuses/update', { status: piece, in_reply_to_status_id: replyTo }, (err, data, response) => { - if (err) { - success = false - console.error(err) - } else { - replyTo = data.id_str - } - }) - }) - - if (success) console.log(`[INFO] Tweeted ${post}`) -} - -// downloads a file from the given url and saves it to temp dir -function downloadFile(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 -function twitterFormat(post) { - post = post.replace('
', '\n') // new lines - post = post.replace('

', '') - post = post.replace('

', '\n') - return post -} - -// poll on mastodon status updates -setInterval(() => { - - mastodon.get('timelines/home', {}).then(resp => { - let lastPostDateRecord = lastPostDate - - resp.data - .filter(it => it.account.username === config.mastodon_user) - .filter(it => Date.parse(it.created_at) > 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 => - downloadFile(media.url, media.id) - ) - Promise.all(downloads) - .then(files => { - twitterPostWithMedias(twitterFormat(status.content), 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 { - twitterPost(twitterFormat(status.content)) - } - }) - - lastPostDate = lastPostDateRecord - - }) - -}, 10000) diff --git a/oauth.js b/oauth.js new file mode 100644 index 0000000..23925ba --- /dev/null +++ b/oauth.js @@ -0,0 +1,58 @@ + +const express = require('express') +const fs = require('fs') +const https = require('https') +const passport = require('passport') +const TwitterStrategy = require('passport-twitter').Strategy +let config = require('./config.js') +let storage = require('./storage.js') + + +let oauth = { + start: function() { + let app = express() + + app.use(require('express-session')({ + secret: storage.data.sess_secret, + resave: true, + saveUninitialized: true + })); + + app.use(passport.initialize()) + app.use(passport.session({ secret: storage.data.sess_secret })) + + passport.use(new TwitterStrategy({ + consumerKey: config.twitter_consumer_key, + consumerSecret: config.twitter_consumer_secret, + callbackURL: `${config.callback_url}${config.callback_path}` + }, + (token, tokenSecret, profile, cb) => { + storage.data.token = token + storage.data.token_secret = tokenSecret + storage.save() + console.log("[INFO] Got tokens, restart!") + } + )) + + console.log('[INFO] Starting HTTPS webserver for OAuth') + + app.get('/', passport.authenticate('twitter')) + + app.get(config.callback_path, passport.authenticate('twitter', { failureRedirect: '/failed' }), (req, res) => { + res.redirect("/success") + }) + + app.get('/failed', (req, res) => res.send('Authentication failed')) + app.get('/success', (req, res) => res.send('Authentication success. Restart XTweet when it stops')) + + https.createServer({ + key: fs.readFileSync(config.key), + cert: fs.readFileSync(config.cert), + passphrase: config.passphrase + }, app).listen(config.port, config.bind_ip) + + console.log(`[INFO] Server listening on ${config.bind_ip}:${config.port}`) + } +} + +module.exports = oauth; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b2a0e8c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "name": "xtweet", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "mastodon": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/mastodon/-/mastodon-1.2.2.tgz", - "integrity": "sha512-ixcYkzn6SorH8U2jNc1vwiX89EiVMjzd2aDYFtr191YY9rdoVo+owI6cQo2EjUnzg2RN9WxyBJ9KDuw+R4lt+w==", - "requires": { - "bluebird": "^3.1.5", - "mime": "^1.3.4", - "request": "^2.68.0" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "twit": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/twit/-/twit-2.2.11.tgz", - "integrity": "sha512-BkdwvZGRVoUTcEBp0zuocuqfih4LB+kEFUWkWJOVBg6pAE9Ebv9vmsYTTrfXleZGf45Bj5H3A1/O9YhF2uSYNg==", - "requires": { - "bluebird": "^3.1.5", - "mime": "^1.3.4", - "request": "^2.68.0" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - } - } -} diff --git a/package.json b/package.json index a755f95..f0750e1 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,27 @@ "version": "1.0.0", "description": "Cross tweet from Mastodon", "main": "main.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "scripts": {}, + "bin": { + "xtweet": "app.js" }, - "author": "", + "repository": { + "type": "git", + "url": "https://git.lattuga.net/ekardnam/XTweet.git" + }, + "bugs": { + "email": "ekardnam@autistici.org", + "url": "https://git.lattuga.net/ekardnam/XTweet/issues" + }, + "author": "ekardnam ", "license": "AGPL-3.0-or-later", "dependencies": { + "express": "^4.17.1", + "express-session": "^1.16.2", "mastodon": "^1.2.2", "mime-types": "^2.1.24", + "passport": "^0.4.0", + "passport-twitter": "^1.0.4", "twit": "^2.2.11" } } diff --git a/storage.js b/storage.js new file mode 100644 index 0000000..92bb08e --- /dev/null +++ b/storage.js @@ -0,0 +1,20 @@ + +const fs = require('fs') + +const STORAGE_FILE = 'storage.json' + +let storage = { + init: function() { + if (!fs.existsSync(STORAGE_FILE)) { + fs.writeFileSync(STORAGE_FILE, '{}') + } + let raw = fs.readFileSync(STORAGE_FILE) + this.data = JSON.parse(raw) + }, + + save: function() { + fs.writeFileSync(STORAGE_FILE, JSON.stringify(this.data)) + } +} + +module.exports = storage;