├── .gitignore ├── .rnd ├── src ├── styles │ ├── styles.scss │ ├── _variables.scss │ ├── _layout.scss │ └── _typography.scss └── scripts │ └── scripts.js ├── robots.txt ├── handlebars-helpers ├── equals.js ├── not_equals.js └── for.js ├── views ├── profile.handlebars ├── 404.handlebars ├── partials │ └── header.handlebars ├── about.handlebars ├── admin.handlebars ├── post.handlebars ├── home.handlebars └── layouts │ └── main.handlebars ├── bot ├── README.md ├── responses.js ├── script.js └── bot.js ├── examples ├── tracery │ ├── README.md │ └── script.js ├── replies │ ├── README.md │ └── bot │ │ └── responses.js └── generative-art-bot │ ├── README.md │ └── script.js ├── tracery ├── tracery.js └── grammar.json ├── watch.json ├── routes ├── webhook.js ├── outbox.js ├── pubsub.js ├── salmon.js ├── well-known.js ├── bot.js ├── admin.js ├── post.js ├── delete-post.js ├── inbox.js ├── index.js └── feed.js ├── public ├── humans.txt └── libs │ └── bootstrap │ └── bootstrap.min.js ├── helpers ├── keys.js ├── cron-schedules.js ├── general.js ├── colorbrewer.js └── db.js ├── LICENSE.md ├── package.json ├── server.js ├── README.md ├── app.js ├── generators └── joy-division.js └── .glitch-assets /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sessions 3 | -------------------------------------------------------------------------------- /.rnd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botwiki/fediverse-bot/HEAD/.rnd -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "typography"; 3 | @import "layout"; 4 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: MastoPeek v0.7.2 - https://mastopeek.app-dist.eu 2 | User-agent: fediverse.space crawler 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /handlebars-helpers/equals.js: -------------------------------------------------------------------------------- 1 | module.exports = function(arg1, arg2, options){ 2 | return (arg1 == arg2) ? options.fn(this) : options.inverse(this); 3 | } -------------------------------------------------------------------------------- /handlebars-helpers/not_equals.js: -------------------------------------------------------------------------------- 1 | module.exports = function(arg1, arg2, options){ 2 | console.log(arg1, arg2); 3 | return (arg1 != arg2) ? options.fn(this) : options.inverse(this); 4 | } -------------------------------------------------------------------------------- /handlebars-helpers/for.js: -------------------------------------------------------------------------------- 1 | module.exports = function(from, to, incr, block){ 2 | var accum = ''; 3 | for(var i = from; i <= to; i += incr) 4 | accum += block.fn(i); 5 | return accum; 6 | } -------------------------------------------------------------------------------- /views/profile.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |
-------------------------------------------------------------------------------- /views/404.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 |

4 | Page not found. 5 |

6 |

7 | Main page 8 |

9 |
-------------------------------------------------------------------------------- /bot/README.md: -------------------------------------------------------------------------------- 1 | # Bot logic 2 | 3 | The files in the `/bot` folder contain the source code for the bot's behavior. 4 | 5 | - Inside `bot.js` you'll find internal methods, like `create_post` and `delete_post`. 6 | - To change bot's responses to messages it receives, see `responses.js`. 7 | -------------------------------------------------------------------------------- /examples/tracery/README.md: -------------------------------------------------------------------------------- 1 | # Tracery bot 2 | 3 | 1. Update `tracery/grammar.json` with your Tracery grammar. You can find Tracery tutorials [on Botwiki](https://botwiki.org/resources/twitterbots/#tutorials-tracery-cbdq). 4 | 2. Use the code from `examples/tracery/script.js` to update your `bot/script.js` file. -------------------------------------------------------------------------------- /tracery/tracery.js: -------------------------------------------------------------------------------- 1 | const tracery = require('tracery-grammar'), 2 | raw_grammar = require(__dirname + '/grammar.json'), 3 | processed_grammar = tracery.createGrammar(raw_grammar); 4 | 5 | processed_grammar.addModifiers(tracery.baseEngModifiers); 6 | 7 | module.exports.grammar = processed_grammar; -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | https://www.happyhues.co/palettes/17 3 | */ 4 | 5 | $background: #fef6e4; 6 | $headline: #001858; 7 | $paragraph: #172c66; 8 | $button: #f582ae; 9 | $button-text: #001858; 10 | 11 | $stroke: #001858; 12 | $main: #f3d2c1; 13 | $highlight: #fef6e4; 14 | $secondary: #8bd3dd; 15 | $tertiary: #f582ae; -------------------------------------------------------------------------------- /watch.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": { 3 | "include": [ 4 | "^package\\.json$" 5 | ] 6 | }, 7 | "restart": { 8 | "exclude": [ 9 | "^src/", 10 | "^public/", 11 | "^dist/" 12 | ], 13 | "include": [ 14 | "^routes/", 15 | "^helpers/", 16 | "\\.js$", 17 | "\\.handlebars$", 18 | "\\.json$", 19 | ".env" 20 | ] 21 | }, 22 | "throttle": 2000 23 | } 24 | -------------------------------------------------------------------------------- /examples/replies/README.md: -------------------------------------------------------------------------------- 1 | **Work in progress** 2 | 3 | ![A bot replying to a message](https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fbot-replies.png?1539088559939) 4 | 5 | # Bot replies 6 | 7 | To use bot replies, update `bot/responses.js` with a function that returns a reply message. 8 | 9 | 10 | TODO: 11 | 12 | - correctly dedupe events 13 | - add support for private messages 14 | - send notification when posting reply 15 | 16 | -------------------------------------------------------------------------------- /routes/webhook.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ), 2 | url = require( 'url' ), 3 | util = require( 'util' ), 4 | express = require( 'express' ), 5 | router = express.Router(); 6 | 7 | router.get( '/', function( req, res ) { 8 | const urlParts = url.parse( req.url, true ); 9 | 10 | console.log( '/webhook', urlParts ); 11 | 12 | res.setHeader( 'Content-Type', 'application/json' ); 13 | res.send( JSON.stringify( { 14 | error: null 15 | } ) ); 16 | } ); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /views/partials/header.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 |

{{bot_name}}

10 |
@bot@{{project_name}}.glitch.me
11 |

{{{bot_description}}}

12 |
13 |
14 |
-------------------------------------------------------------------------------- /routes/outbox.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | util = require('util'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | express = require('express'), 6 | router = express.Router(); 7 | 8 | router.all('/', (req, res) => { 9 | const urlParts = url.parse(req.url, true); 10 | 11 | console.log('/outbox', urlParts); 12 | 13 | res.setHeader('Content-Type', 'application/json'); 14 | res.send(JSON.stringify({ 15 | error: null 16 | })); 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /routes/pubsub.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | util = require('util'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | express = require('express'), 6 | router = express.Router(); 7 | 8 | router.all('/', (req, res) => { 9 | const urlParts = url.parse(req.url, true); 10 | 11 | console.log('/pubsub', urlParts); 12 | 13 | res.setHeader('Content-Type', 'application/json'); 14 | res.send(JSON.stringify({ 15 | error: null 16 | })); 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /routes/salmon.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | util = require('util'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | express = require('express'), 6 | router = express.Router(); 7 | 8 | router.all('/', (req, res) => { 9 | const urlParts = url.parse(req.url, true); 10 | 11 | console.log('/salmon', urlParts); 12 | 13 | res.setHeader('Content-Type', 'application/json'); 14 | res.send(JSON.stringify({ 15 | error: null 16 | })); 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /views/about.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 |
4 |
5 |
6 | Glitch Fediverse Bot 7 |
8 |

9 | Work in progress. See related GitHub repo. 10 |

11 |
12 |
13 |

14 | Main page 15 |

16 |
17 | -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | | | | | | | 3 | | |__ _ _ _ __ ___ __ _ _ __ ___ | |___ _| |_ 4 | | '_ \| | | | '_ ` _ \ / _` | '_ \/ __|| __\ \/ / __| 5 | | | | | |_| | | | | | | (_| | | | \__ \| |_ > <| |_ 6 | |_| |_|\__,_|_| |_| |_|\__,_|_| |_|___(_)__/_/\_\\__| 7 | 8 | /* 9 | 10 | glitch.com/edit/#!/glitch-fediverse-bot 11 | 12 | Created by twitter.com/fourtonfish. 13 | 14 | humanstxt.org 15 | 16 | */ 17 | -------------------------------------------------------------------------------- /examples/tracery/script.js: -------------------------------------------------------------------------------- 1 | const express = require( 'express' ), 2 | router = express.Router(), 3 | grammar = require( __dirname + '/../tracery/tracery.js' ).grammar; 4 | 5 | module.exports = function(){ 6 | const bot = require( __dirname + '/bot.js' ), 7 | content = grammar.flatten( '#origin#' ); 8 | 9 | console.log( 'posting new message...' ); 10 | 11 | bot.createPost( { 12 | type: 'Note', // See www.w3.org/ns/activitystreams#objects 13 | content: content 14 | }, function( err, message ){ 15 | if ( err ){ 16 | console.log( err ); 17 | } 18 | } ); 19 | }; 20 | -------------------------------------------------------------------------------- /routes/well-known.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | util = require('util'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | express = require('express'), 6 | router = express.Router(); 7 | 8 | router.get('/webfinger', (req, res) => { 9 | const urlPparts = url.parse(req.url, true), 10 | query = urlPparts.query; 11 | 12 | console.log('webfinger request', query, { 13 | subject: query.resource, 14 | links: bot.links 15 | }); 16 | 17 | res.json({ 18 | subject: query.resource, 19 | links: bot.links 20 | }); 21 | }); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /views/admin.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 |
-------------------------------------------------------------------------------- /src/scripts/scripts.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | $('[data-confirm]').click(function(ev){ 5 | // ev.preventDefault(); 6 | var confirmActionText = $(this).data('confirm'); 7 | 8 | return confirm(confirmActionText); 9 | }); 10 | 11 | $('.post-date').each(() => { 12 | let $date = $(this); 13 | const date = $date.attr('title'); 14 | $date.attr('title', moment(moment.utc(date).toDate()).local().format('YYYY-MM-DD HH:mm:ss')); 15 | }); 16 | 17 | $('.post-date-formatted').each(() => { 18 | let $date = $(this); 19 | const date = $date.html(); 20 | $date.html(moment(moment.utc(date).toDate()).local().format('YYYY-MM-DD HH:mm:ss')); 21 | }); 22 | 23 | })(jQuery); 24 | -------------------------------------------------------------------------------- /helpers/keys.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ), 2 | util = require( 'util' ), 3 | generate_rsa_keypair = require( 'generate-rsa-keypair' ), 4 | pem = require( 'pem' ), 5 | pubkey_path = '.data/rsa/pubKey', 6 | privkey_path = '.data/rsa/privKey'; 7 | 8 | module.exports = { 9 | generateKeys: function( cb ) { 10 | console.log( 'generating keys...' ); 11 | 12 | try{ 13 | fs.mkdirSync( '.data/rsa' ); 14 | } catch( err ){ /* noop */ } 15 | 16 | var pair = generate_rsa_keypair( ); 17 | 18 | fs.writeFileSync( privkey_path, pair.private ); 19 | fs.writeFileSync( pubkey_path, pair.public ); 20 | 21 | if ( cb ){ 22 | cb( null ); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /routes/bot.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | util = require('util'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | express = require('express'), 6 | router = express.Router(); 7 | 8 | router.get('/', (req, res) => { 9 | const urlParts = url.parse(req.url, true); 10 | console.log(req.headers['user-agent'], req.headers['user-agent'].indexOf('mastodon')); 11 | if (req.headers['user-agent'].indexOf('mastodon') !== -1 || (req.query.debug && req.query.debug !== '')){ 12 | res.setHeader('Content-Type', 'application/json'); 13 | // console.log(bot.info); 14 | // res.send(JSON.stringify(bot.info)); 15 | res.json(bot.info); 16 | } 17 | else{ 18 | res.redirect('/'); 19 | } 20 | }); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /tracery/grammar.json: -------------------------------------------------------------------------------- 1 | { 2 | "origin": ["#phrase#"], 3 | "phrase": [ 4 | "#count_thing#", 5 | "#count_thing#", 6 | "#count_thing#", 7 | "#count_thing#", 8 | "#count_thing#.", 9 | "#count_thing#.", 10 | "#count_thing#, #count_thing#" 11 | ], 12 | "count_thing": [ 13 | "#small_count_thing#", 14 | "#small_count_thing#", 15 | "#small_count_thing#", 16 | "#large_count_thing#" 17 | ], 18 | "small_count_thing": [ 19 | "1 #thing#", 20 | "2 #thing.s#", 21 | "3 #thing.s#" 22 | ], 23 | "large_count_thing": [ 24 | "4 #thing.s#", 25 | "5 #thing.s#", 26 | "6 #thing.s#", 27 | "7 #thing.s#" 28 | ], 29 | "thing": [ 30 | "candle", 31 | "circle", 32 | "cup", 33 | "mark", 34 | "ward", 35 | "body", 36 | "wand", 37 | "seal" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #fef6e4; 3 | } 4 | 5 | main{ 6 | max-width: 720px; 7 | margin: 0 auto; 8 | } 9 | 10 | img{ 11 | max-width: 100%; 12 | } 13 | 14 | blockquote{ 15 | /* Courtesy of css-tricks.com. */ 16 | background: $main; 17 | border-left: 10px solid $tertiary; 18 | // max-width: $max-content-width; 19 | margin: 2em auto; 20 | padding: 0.5em 1em 1em 0; 21 | quotes: "\201C""\201D""\2018""\2019"; 22 | 23 | .col-md-6 &{ 24 | padding: 1em; 25 | } 26 | 27 | &:before { 28 | color: $tertiary; 29 | content: '“'; 30 | font-size: 4em; 31 | line-height: 0.1em; 32 | margin-right: 0.25em; 33 | vertical-align: -0.4em; 34 | } 35 | 36 | p, cite { 37 | line-height: 1.5em; 38 | padding: 0 40px; 39 | } 40 | 41 | cite{ 42 | color: $paragraph + #222; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /helpers/cron-schedules.js: -------------------------------------------------------------------------------- 1 | /* Common cron schedules. Visit the cron package documentation at https://www.npmjs.com/package/cron to see how to create your own. */ 2 | 3 | module.exports = { 4 | EVERY_FIVE_SECONDS: '*/5 * * * * *', 5 | EVERY_TEN_SECONDS: '*/10 * * * * *', 6 | EVERY_THIRTY_SECONDS: '*/30 * * * * *', 7 | EVERY_MINUTE: '* * * * *', 8 | EVERY_FIVE_MINUTES: '*/5 * * * *', 9 | EVERY_TEN_MINUTES: '*/10 * * * *', 10 | EVERY_THIRTY_MINUTES: '*/30 * * * *', 11 | EVERY_HOUR: '0 * * * *', 12 | EVERY_TWO_HOURS: '0 */2 * * *', 13 | EVERY_THREE_HOURS: '0 */3 * * *', 14 | EVERY_FOUR_HOURS: '0 */4 * * *', 15 | EVERY_SIX_HOURS: '0 */6 * * *', 16 | EVERY_TWELVE_HOURS: '0 */12 * * *', 17 | EVERY_DAY_MIDNIGHT: '0 0 * * *', 18 | EVERY_DAY_MORNING: '0 8 * * *', 19 | EVERY_DAY_NOON: '0 12 * * *', 20 | EVERY_DAY_AFTERNOON: '0 14 * * *', 21 | EVERY_DAY_EVENING: '0 19 * * *' 22 | }; -------------------------------------------------------------------------------- /views/post.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 |
4 | {{#equals post.type "Note"}} 5 |
6 | 7 | {{#if post.content}} 8 |

{{{post.content}}}

9 | {{/if}} 10 | 11 | {{#each post.attachment}} 12 |

{{this.content}}

13 | {{#if this.content}} 14 | 15 | {{/if}} 16 | {{/each}} 17 |
18 | {{/equals}} 19 |
20 |

21 | Main page 22 | {{#if is_admin}} 23 | Delete 24 | {{/if}} 25 |

26 |
27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Stefan Bohacek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/generative-art-bot/README.md: -------------------------------------------------------------------------------- 1 | ![An example of a bot posting generated image](https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot-with-image.png) 2 | 3 | # Generative art bot 4 | 5 | This is an example of a bot that generates images. It uses an image generator from [generative-art-bot](https://glitch.com/edit/#!/generative-art-bot). 6 | 7 | (Note that the code needed to be slightly modified to work with this project.) 8 | 9 | To try this example, copy the content of `examples/generative-art-bot/routes/bot-endpoint.js` to `routes/bot-endpoint.js` file and run your bot using its endpoint. 10 | 11 | By default, the images are stored in the `.data/img` folder. Glitch only provides ~128MB of storage (there are technical limitations to using the `assets` folder, which gives you additional ~500MB), but you can upload your images to NeoCities, which comes with a free 1GB of space, and has a paid plan ([$5/month](https://neocities.org/supporter)) that offers 50GB. 12 | 13 | Simply sign up for an account at [neocities.org](https://neocities.org/), and save your login information to the `.env` file as `NEOCITIES_USERNAME` and `NEOCITIES_PASSWORD`. 14 | 15 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | body{ 4 | font-family: 'Open Sans', sans-serif; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6{ 8 | font-family: 'Source Serif Pro', serif; 9 | font-weight: bold; 10 | } 11 | 12 | 13 | #about-bot.jumbotron{ 14 | background: $tertiary; 15 | border-top-left-radius: 0; 16 | border-top-right-radius: 0; 17 | color: $highlight; 18 | a{ 19 | color: $highlight; 20 | text-decoration: underline; 21 | } 22 | .bot-username{ 23 | font-size: 1rem; 24 | color: $highlight; 25 | } 26 | } 27 | 28 | .card{ 29 | color: $paragraph; 30 | border: solid 3px $stroke; 31 | 32 | .card-footer{ 33 | background: #f3d2c1; 34 | 35 | a.post-date{ 36 | color: $paragraph; 37 | } 38 | } 39 | } 40 | 41 | a, a:hover{ 42 | font-weight: bold; 43 | color: $paragraph; 44 | } 45 | 46 | .card-text{ 47 | a{ 48 | text-decoration: underline; 49 | } 50 | } 51 | 52 | .btn{ 53 | background: $tertiary; 54 | transition: none; 55 | 56 | &:hover{ 57 | transition: none; 58 | } 59 | } 60 | 61 | .btn-secondary{ 62 | color: $button-text; 63 | border: 1px solid $button-text; 64 | background: $highlight; 65 | } 66 | 67 | .text-muted{ 68 | color: $paragraph + #222; 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glitch-fediverse-bot", 3 | "version": "0.0.1", 4 | "description": "A self-hosted fediverse bot, powered by Glitch", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "cron": "^1.8.2", 11 | "express": "^4.16.3", 12 | "sqlite3": "^4.0.0", 13 | "express-handlebars": "^3.0.0", 14 | "body-parser": "^1.18.3", 15 | "tracery-grammar": "^2.7.3", 16 | "activitystrea.ms": "^2.1.3", 17 | "pem": "^1.13.0", 18 | "momentjs": "^2.0.0", 19 | "rss": "^1.2.2", 20 | "pubsubhubbub": "^0.4.1", 21 | "node-openssl-cert": "^0.0.47", 22 | "generate-rsa-keypair": "^0.1.2", 23 | "node-sass-middleware": "^0.11.0", 24 | "express-babelify-middleware": "^0.2.1", 25 | "express-session": "^1.15.6", 26 | "connect-sqlite3": "^0.9.11", 27 | "canvas": "^2.0.0-alpha.17", 28 | "color-scheme": "^1.0.1", 29 | "gifencoder": "^1.1.0", 30 | "neocities": "^0.0.3", 31 | "jsdom": "^12.1.0", 32 | "dotenv": "^8.2.0" 33 | }, 34 | "engines": { 35 | "node": "8.x" 36 | }, 37 | "repository": { 38 | "url": "https://glitch.com/edit/#!/glitch-fediverse-bot" 39 | }, 40 | "license": "MIT", 41 | "keywords": [ 42 | "node", 43 | "glitch", 44 | "fediverse", 45 | "bot" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /bot/responses.js: -------------------------------------------------------------------------------- 1 | /* 2 | Here you can modify how the bot responds to messages it receives. 3 | */ 4 | 5 | module.exports = function(data, callback_function) { 6 | /* 7 | At the end of this function we need to pass an error message and a response text. 8 | Let's set up some default values. 9 | */ 10 | 11 | var error = null, 12 | response = 'Hello 👋'; 13 | 14 | /* 15 | The data object this function receives looks like this: 16 | 17 | data = { 18 | payload: 'The original data object.', 19 | message_body: 'The content of the message sent to the bot.', 20 | message_from: 'The URL of the message sender.' 21 | } 22 | 23 | message_body and message_from come from the payload object, so we can access them more conveniently. If we need more details, we can get those from the payload object itself. 24 | 25 | */ 26 | 27 | console.log(`new message from ${data.message_from}:`) 28 | console.log(data.message_body); 29 | 30 | /* 31 | We can modify the response text. 32 | */ 33 | 34 | if (data.message_body.toLowerCase().indexOf('hello') > -1){ 35 | response = 'Hi 👋'; 36 | } 37 | 38 | /* 39 | Finally, we pass the error and reply message to the callback function that sends it to the author of the message that the bot received and saves it to the post database. 40 | */ 41 | 42 | callback_function(error, response); 43 | }; 44 | -------------------------------------------------------------------------------- /examples/replies/bot/responses.js: -------------------------------------------------------------------------------- 1 | /* 2 | Here you can modify how the bot responds to messages it receives. 3 | */ 4 | 5 | module.exports = function(data, callback_function) { 6 | /* 7 | At the end of this function we need to pass an error message and a response text. 8 | Let's set up some default values. 9 | */ 10 | 11 | var error = null, 12 | response = 'Hello 👋'; 13 | 14 | /* 15 | The data object this function receives looks like this: 16 | 17 | data = { 18 | payload: 'The original data object.', 19 | message_body: 'The content of the message sent to the bot.', 20 | message_from: 'The URL of the message sender.' 21 | } 22 | 23 | message_body and message_from come from the payload object, so we can access them more conveniently. If we need more details, we can get those from the payload object itself. 24 | 25 | */ 26 | 27 | console.log(`new message from ${data.message_from}:`) 28 | console.log(data.message_body); 29 | 30 | /* 31 | We can modify the response text. 32 | */ 33 | 34 | if (data.message_body.toLowerCase().indexOf('hello') > -1){ 35 | response = 'Hi 👋'; 36 | } 37 | 38 | /* 39 | Finally, we pass the error and reply message to the callback function that sends it to the author of the message that the bot received and saves it to the post database. 40 | */ 41 | 42 | callback_function(error, response); 43 | }; 44 | -------------------------------------------------------------------------------- /examples/generative-art-bot/script.js: -------------------------------------------------------------------------------- 1 | const express = require( 'express' ), 2 | helpers = require( __dirname + '/../helpers/general.js' ), 3 | ColorScheme = require('color-scheme'), 4 | generators = { 5 | triangular_mesh: require(__dirname + '/../generators/triangular-mesh.js') 6 | }, 7 | grammar = require( __dirname + '/../tracery/tracery.js' ).grammar; 8 | 9 | module.exports = function(){ 10 | const bot = require( __dirname + '/bot.js' ), 11 | content = grammar.flatten( '#origin#' ), 12 | scheme = new ColorScheme; 13 | 14 | /* See https://www.npmjs.com/package/color-scheme#schemes on how to use ColorScheme. */ 15 | 16 | scheme.from_hex( helpers.getRandomHex().replace( '#','' ) ) 17 | .scheme( 'mono' ) 18 | .variation( 'soft' ); 19 | 20 | generators.triangular_mesh( { 21 | width: 800, 22 | height: 360, 23 | colors: scheme.colors() 24 | }, function( err, imgData ){ 25 | console.log( 'posting new image...', { imgUrl: `${ bot.bot_url }/${ imgData.path }` } ); 26 | 27 | var imgName = imgData.path.replace( 'img/', '' ); 28 | 29 | bot.createPost( { 30 | type: 'Note', 31 | content: content, 32 | attachment: [ 33 | { 34 | url: bot.bot_url, 35 | content: 'Abstract art' // Image description here. 36 | } 37 | ] 38 | }, function( err, message ){ 39 | if ( err ){ 40 | console.log( err ); 41 | } 42 | } ); 43 | } ); 44 | }; 45 | -------------------------------------------------------------------------------- /bot/script.js: -------------------------------------------------------------------------------- 1 | const express = require( 'express' ), 2 | helpers = require( __dirname + '/../helpers/general.js' ), 3 | ColorScheme = require('color-scheme'), 4 | colorbrewerColors = require( __dirname + '/../helpers/colorbrewer.js' ), 5 | generators = { 6 | joy_division: require(__dirname + '/../generators/joy-division.js') 7 | }, 8 | grammar = require( __dirname + '/../tracery/tracery.js' ).grammar; 9 | 10 | module.exports = function(){ 11 | const bot = require( __dirname + '/bot.js' ), 12 | content = grammar.flatten( '#origin#' ), 13 | scheme = new ColorScheme; 14 | 15 | /* See https://www.npmjs.com/package/color-scheme#schemes on how to use ColorScheme. */ 16 | 17 | scheme.from_hex( helpers.getRandomHex().replace( '#','' ) ) 18 | .scheme( 'mono' ) 19 | .variation( 'soft' ); 20 | 21 | generators.joy_division( { 22 | width: 640, 23 | height: 480, 24 | colors: helpers.randomFromArray( colorbrewerColors ), 25 | animate: true, 26 | save: true 27 | }, function( err, imgURL ){ 28 | console.log( 'posting new image...', imgURL ); 29 | 30 | bot.createPost( { 31 | type: 'Note', 32 | // content: content, 33 | attachment: [ 34 | { 35 | url: imgURL, 36 | content: content // Image description here. 37 | } 38 | ] 39 | }, function( err, message ){ 40 | if ( err ){ 41 | console.log( err ); 42 | } 43 | } ); 44 | } ); 45 | }; 46 | -------------------------------------------------------------------------------- /routes/admin.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | session = require('express-session'), 3 | router = express.Router(), 4 | moment = require('moment'), 5 | db = require(__dirname + '/../helpers/db.js'), 6 | bot = require(__dirname + '/../bot/bot.js'); 7 | 8 | router.get('/', (req, res) => { 9 | res.render('../views/admin.handlebars', { 10 | project_name: process.env.PROJECT_DOMAIN, 11 | bot_avatar_url: process.env.BOT_AVATAR_URL, 12 | bot_username: process.env.BOT_USERNAME, 13 | bot_name: process.env.BOT_NAME, 14 | bot_description: process.env.BOT_DESCRIPTION 15 | }); 16 | }); 17 | 18 | router.get('/logout', (req, res) => { 19 | req.session.is_admin = false; 20 | req.session.save(); 21 | 22 | console.log('admin logged out'); 23 | console.log(req.body); 24 | res.redirect('/'); 25 | }); 26 | 27 | router.post('/', (req, res) => { 28 | if (process.env.ADMIN_PASSWORD && req.body.password){ 29 | if (req.body.password === process.env.ADMIN_PASSWORD){ 30 | req.session.is_admin = true; 31 | req.session.save(); 32 | console.log('saving session...', req.session.is_admin); 33 | 34 | req.session.save((err) => { 35 | console.log('admin logged in'); 36 | res.redirect('/'); 37 | }); 38 | } 39 | else{ 40 | req.session.is_admin = false; 41 | console.log('failed login attempt'); 42 | console.log(req.body); 43 | res.redirect('/admin'); 44 | } 45 | } 46 | }); 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | if (!process.env.PROJECT_NAME || !process.env.PROJECT_ID){ 2 | require('dotenv').config(); 3 | } 4 | 5 | const fs = require('fs'), 6 | imgPath = './.data/img'; 7 | 8 | if (!fs.existsSync(imgPath)){ 9 | fs.mkdirSync(imgPath); 10 | } 11 | 12 | const app = require(__dirname + "/app.js"), 13 | db = require(__dirname + "/helpers/db.js"), 14 | bot = require(__dirname + "/bot/bot.js"), 15 | CronJob = require("cron").CronJob, 16 | cronSchedules = require(__dirname + "/helpers/cron-schedules.js"); 17 | 18 | db.init(); 19 | 20 | /*********************************************/ 21 | /* FOR DEBUGGING */ 22 | 23 | // db.dropTable('Posts'); 24 | // db.dropTable('Followers'); 25 | // db.dropTable('Events'); 26 | 27 | db.getFollowers((err, data) => { 28 | console.log('Followers:', data); 29 | }); 30 | 31 | // db.getPosts((err, data) => { 32 | // console.log('Posts:', data); 33 | // }); 34 | 35 | // db.getEvents((err, data) => { 36 | // console.log('Events:', data); 37 | // }); 38 | 39 | // bot.script(); 40 | 41 | /* DEBUGGING END */ 42 | /*********************************************/ 43 | 44 | /* Schedule your bot. See helpers/cron-schedules.js for common schedules, or the cron package documentation at https://www.npmjs.com/package/cron to create your own.*/ 45 | 46 | // (new CronJob(cronSchedules.EVERY_THIRTY_SECONDS, () => {bot.script()})).start(); 47 | (new CronJob(cronSchedules.EVERY_SIX_HOURS, () => {bot.script()})).start(); 48 | 49 | bot.script() 50 | 51 | const listener = app.listen(process.env.PORT, () => { 52 | console.log(`app is running on port ${listener.address().port}...`); 53 | }); 54 | -------------------------------------------------------------------------------- /routes/post.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | router = express.Router(), 3 | moment = require('moment'), 4 | db = require(__dirname + '/../helpers/db.js'); 5 | 6 | router.get('/:id', (req, res) => { 7 | const postID = req.params.id; 8 | 9 | db.getPost(postID, (err, post_data) => { 10 | if (post_data){ 11 | post_data.date_formatted = moment(post_data.date).fromNow();; 12 | 13 | try{ 14 | post_data.attachment = JSON.parse(post_data.attachment); 15 | } catch(err){ /*noop*/ } 16 | 17 | 18 | res.render('../views/post.handlebars', { 19 | project_name: process.env.PROJECT_DOMAIN, 20 | bot_url: `https://${process.env.PROJECT_DOMAIN}.glitch.me/`, 21 | bot_avatar_url: process.env.BOT_AVATAR_URL, 22 | bot_username: process.env.BOT_USERNAME, 23 | bot_description: process.env.BOT_DESCRIPTION, 24 | is_admin: req.session.is_admin, 25 | page_title: `${process.env.BOT_NAME}: ${post_data.date}`, 26 | page_description: post_data.content, 27 | post: post_data 28 | }); 29 | } 30 | else{ 31 | res.render('../views/404.handlebars', { 32 | project_name: process.env.PROJECT_DOMAIN, 33 | bot_url: `https://${process.env.PROJECT_DOMAIN}.glitch.me/`, 34 | bot_avatar_url: process.env.BOT_AVATAR_URL, 35 | bot_username: process.env.BOT_USERNAME, 36 | bot_description: process.env.BOT_DESCRIPTION, 37 | page_title: `${process.env.BOT_NAME}: Page not found`, 38 | is_admin: req.session.is_admin 39 | }); 40 | } 41 | }); 42 | }); 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /routes/delete-post.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | router = express.Router(), 3 | moment = require('moment'), 4 | bot = require(__dirname + '/../bot/bot.js'), 5 | db = require(__dirname + '/../helpers/db.js'); 6 | 7 | router.get('/:id', (req, res) => { 8 | let isAdmin = req.session.is_admin, 9 | postID = req.params.id; 10 | 11 | if (isAdmin){ 12 | if (postID === 'all'){ 13 | console.log({ 14 | 'delete post': 'all of them' 15 | }); 16 | 17 | db.getPosts({ limit: 0 }, (err, data) => { 18 | if (err){ 19 | console.log(err); 20 | } else { 21 | if (data && data.posts && data.posts.length > 0){ 22 | data.posts.forEach((post) => { 23 | db.deletePost(post.id, bot, (err) => { 24 | if (err){ 25 | console.log(`error deleting post ${ post.id }`, err); 26 | } else { 27 | console.log(`deleted post ${ post.id }`); 28 | } 29 | }); 30 | }); 31 | } 32 | } 33 | }); 34 | 35 | /* TODO: Use promises to redirect after all posts are deleted. */ 36 | 37 | res.redirect('/'); 38 | 39 | } else { 40 | console.log({ 41 | 'delete post': postID 42 | }); 43 | 44 | db.deletePost(postID, bot, (err) => { 45 | if (err){ 46 | console.log(`error deleting post ${postID}`, err); 47 | } else { 48 | console.log(`deleted post ${postID}`); 49 | } 50 | }); 51 | res.redirect('/'); 52 | } 53 | } 54 | else{ 55 | res.redirect('/admin'); 56 | } 57 | }); 58 | 59 | module.exports = router; 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Fediverse bot feed](https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Ffeed-comb.png) 2 | 3 | # Fediverse bot 4 | 5 | Make creative online bots that anyone [in the fediverse](https://en.wikipedia.org/wiki/Fediverse) can follow! This project is [under active development](https://github.com/botwiki/fediverse-bot/issues) and contributions and feature suggestions are welcome. 6 | 7 | - [Import to Glitch](https://glitch.com/#!/import/github/botwiki/fediverse-bot) ([Learn more](https://glitch.com/about)) 8 | 9 | ## Bot administration 10 | 11 | You can log into the admin panel by going to `/admin` and logging in using the password set inside your `.env` file. This will allow you to delete your bot's posts one by one. (Multi-post deletion is coming!) 12 | 13 | ## Bot logic (the back end) 14 | 15 | 1. Update your bot's main script in `bot/script.js`. 16 | 2. Set up your bot's schedule in `server.js`. 17 | 18 | ``` 19 | (new CronJob(cronSchedules.EVERY_SIX_HOURS, () => {bot.script()})).start(); 20 | ``` 21 | 22 | See `helpers/cron-schedules.js` for common schedules, or the cron package documentation at https://www.npmjs.com/package/cron to create your own. 23 | 24 | ## The look of your bot's page (the front end) 25 | 26 | You can update the style files inside `src/styles`. You can use [sass](https://sass-lang.com/guide), it will be compiled using [node-sass-middleware](https://github.com/sass/node-sass-middleware). Update the scripts inside `src/scripts`. 27 | 28 | You can use [ES6](http://es6-features.org/#Constants), you script files will be compiled using [express-babelify-middleware](https://github.com/luisfarzati/express-babelify-middleware). All templates are inside the `views` folder and use [handlebars.js](http://handlebarsjs.com/). 29 | 30 | ## TO-DO: 31 | 32 | [See issues on GitHub.](https://github.com/fourtonfish/fediverse-bot/issues) 33 | 34 | ## Resources: 35 | 36 | - [ActivityPub documentation](https://github.com/w3c/activitypub) (github.com) 37 | - [How to implement a basic ActivityPub server](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/) (blog.joinmastodon.org) 38 | - [What is necessary for Mastodon to be able to fetch my profile and a list of posts from my blog?](https://github.com/tootsuite/mastodon/issues/1441) (github.com) 39 | - [express-activitypub](https://github.com/dariusk/express-activitypub) (github.com) 40 | 41 | ## Debugging/testing 42 | 43 | - [webfinger output](https://fediverse-bot.glitch.me/.well-known/webfinger?resource=acct:bot@fediverse-bot.glitch.me) 44 | - [the Actor object](https://fediverse-bot.glitch.me/bot?debug=true) 45 | -------------------------------------------------------------------------------- /routes/inbox.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | url = require('url'), 3 | crypto = require('crypto'), 4 | util = require('util'), 5 | jsdom = require('jsdom'), 6 | dbHelper = require(__dirname + '/../helpers/db.js'), 7 | bot = require(__dirname + '/../bot/bot.js'), 8 | { JSDOM } = jsdom, 9 | express = require('express'), 10 | router = express.Router(); 11 | 12 | router.post('/', (req, res) => { 13 | let urlParts = url.parse(req.url, true), 14 | payload = req.body; 15 | 16 | console.log('/inbox'); 17 | 18 | console.log(payload.id); 19 | 20 | /* 21 | TODO: Verify the message. 22 | */ 23 | 24 | if (payload.type === 'Follow'){ 25 | 26 | bot.accept(payload, (err, payload, data) => { 27 | if (!err){ 28 | dbHelper.saveFollower(payload, (err, data) => { 29 | console.log(`new follower ${payload.actor} saved`); 30 | }); 31 | } 32 | res.status(200); 33 | }); 34 | } 35 | else if (payload.type === 'Undo'){ 36 | bot.accept(payload, (err, payload, data) => { 37 | if (!err){ 38 | dbHelper.removeFollower(payload, (err, data) => { 39 | console.log(`removed follower ${payload.actor}`); 40 | }); 41 | } 42 | res.status(200); 43 | }); 44 | } 45 | else if (payload.type === 'Create'){ 46 | bot.accept(payload, (err, payload, data) => { 47 | if (!err && payload.object && payload.object.content){ 48 | let dom = new JSDOM(`
${payload.object.content}
`), 49 | message_body = ''; 50 | try { 51 | message_body = dom.window.document.body.firstChild.textContent; 52 | 53 | } catch(err){ /* noop */} 54 | 55 | bot.composeReply({ 56 | payload: payload, 57 | message_from: payload.actor, 58 | message_body: message_body, 59 | }, (err, reply_message) => { 60 | if (!err){ 61 | console.log(err); 62 | console.log('sending reply...'); 63 | bot.sendReply({ 64 | payload: payload, 65 | message_body: message_body, 66 | reply_message: reply_message 67 | }, (err, data) => { 68 | 69 | }); 70 | } 71 | }); 72 | } 73 | res.status(200); 74 | }); 75 | } 76 | else if (payload.type === 'Delete'){ 77 | // console.log('payload', payload); 78 | console.log('Delete /*noop*/'); 79 | res.status(200); 80 | } 81 | else{ 82 | console.log('payload', payload); 83 | res.status(200); 84 | } 85 | }); 86 | 87 | module.exports = router; 88 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | if (!process.env.PROJECT_NAME || !process.env.PROJECT_ID){ 2 | require('dotenv').config(); 3 | } 4 | 5 | const path = require('path'), 6 | express = require('express'), 7 | session = require('express-session'), 8 | SQLiteStore = require('connect-sqlite3')(session), 9 | exphbs = require('express-handlebars'), 10 | bodyParser = require('body-parser'), 11 | pubSubHubbub = require('pubsubhubbub'), 12 | sassMiddleware = require('node-sass-middleware'), 13 | babelify = require('express-babelify-middleware'), 14 | helpers = require(__dirname + '/helpers/general.js'), 15 | db = require(__dirname + '/helpers/db.js'), 16 | app = express(); 17 | 18 | app.use(express.static('public')); 19 | 20 | app.use(bodyParser.json({ 21 | type: 'application/activity+json' 22 | })); 23 | 24 | app.use(bodyParser.urlencoded({ 25 | extended: true 26 | })); 27 | 28 | app.use(session({ 29 | store: new SQLiteStore, 30 | secret: process.env.ADMIN_PASSWORD, 31 | resave: true, 32 | saveUninitialized: true, 33 | cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } 34 | })); 35 | 36 | app.use(sassMiddleware({ 37 | // src: __dirname, 38 | src: __dirname + '/src/styles', 39 | dest: path.join(__dirname, 'public'), 40 | force: true, 41 | // debug: true, 42 | outputStyle: 'compressed', 43 | response: true 44 | })); 45 | 46 | app.use('/js/scripts.js', babelify('src/scripts/scripts.js', { 47 | minify: true 48 | })); 49 | 50 | app.use('/node_modules', express.static(__dirname + '/node_modules/')); 51 | 52 | app.engine('handlebars', exphbs({ 53 | defaultLayout: 'main', 54 | helpers: { 55 | for: require('./handlebars-helpers/for'), 56 | equals: require('./handlebars-helpers/equals') 57 | } 58 | })); 59 | 60 | app.set('views', __dirname + '/views'); 61 | app.set('view engine', 'handlebars'); 62 | 63 | app.use('/', require('./routes/index.js')) 64 | app.use('/admin', require('./routes/admin.js')); 65 | app.use('/bot', require('./routes/bot.js')); 66 | app.use('/delete-post', require('./routes/delete-post.js')); 67 | app.use('/feed', require('./routes/feed.js')); 68 | app.use('/img', express.static(__dirname + '/.data/img/')); 69 | 70 | app.use('/inbox', require('./routes/inbox.js')); 71 | app.use('/outbox', require('./routes/outbox.js')); 72 | app.use('/post', require('./routes/post.js')); 73 | app.use('/pubsub', require('./routes/pubsub.js')); 74 | app.use('/salmon', require('./routes/salmon.js')); 75 | app.use('/webhook', require('./routes/webhook.js')); 76 | app.use('/.well-known', require('./routes/well-known.js')); 77 | 78 | app.get('/js/helpers.js', (req, res) => { 79 | res.sendFile(path.join(__dirname + '/helpers/general.js')); 80 | }); 81 | 82 | module.exports = app; 83 | -------------------------------------------------------------------------------- /views/home.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 | {{#if no_posts}} 4 |

This bot hasn't posted yet.

5 | {{/if}} 6 | {{#each posts}} 7 |
8 | {{#if this.attachment}} 9 | 10 | 11 | 12 | {{/if}} 13 | {{#equals this.type "Note"}} 14 | {{#if this.content}} 15 |
16 | 17 |

{{{this.content}}}

18 |
19 | {{/if}} 20 | {{/equals}} 21 | 27 |
28 | {{/each}} 29 | {{#if has_posts}} 30 | {{#if is_admin}} 31 |
32 |
33 | Delete all posts 34 |
35 |
36 | {{/if}} 37 | {{/if}} 38 |
39 |
40 | {{#if show_pagination}} 41 |
42 |
    43 | {{#if show_previous_page}} 44 |
  • 45 | Previous 46 |
  • 47 | {{/if}} 48 | {{#for 1 page_count 1}} 49 |
  • 54 | {{this}} 55 |
  • 56 | {{/for}} 57 | {{#if show_next_page}} 58 |
  • 59 | Next 60 |
  • 61 | {{/if}} 62 |
63 |
64 | {{/if}} 65 |
66 |
67 | What is this? 68 | {{#if is_admin}} 69 | Log out 70 | {{else}} 71 | {{#if show_admin_link}} 72 | Admin 73 | {{/if}} 74 | {{/if}} 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{page_title}} 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 50 | {{{body}}} 51 |
52 |
53 | 61 |
62 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | express = require('express'), 3 | session = require('express-session'), 4 | router = express.Router(), 5 | moment = require('moment'), 6 | db = require(__dirname + '/../helpers/db.js'), 7 | bot = require(__dirname + '/../bot/bot.js'), 8 | publicKeyPath = '.data/rsa/pubKey'; 9 | 10 | 11 | router.get('/', (req, res) => { 12 | // console.log(req.headers); 13 | // console.log(JSON.stringify(actor)); 14 | 15 | if (req.headers && req.headers['user-agent'] && req.headers['user-agent'].indexOf('Mastodon') !== -1 ){ 16 | console.log(req.headers['user-agent']); 17 | res.setHeader('Content-Type', 'application/json'); 18 | res.send(JSON.stringify(bot.info)); 19 | } 20 | else{ 21 | let page = parseInt(req.query.page) || 1; 22 | 23 | db.getPosts({ 24 | page: page 25 | }, (err, data)=> { 26 | // console.log(posts); 27 | 28 | let noPosts = false; 29 | 30 | if (data && data.posts && data.posts.length > 0){ 31 | data.posts.forEach((post)=> { 32 | post.date_formatted = moment(post.date).fromNow(); 33 | try{ 34 | post.attachment = JSON.parse(post.attachment); 35 | } catch(err){ /*noop*/ } 36 | }); 37 | } else { 38 | noPosts = true; 39 | } 40 | 41 | let showNextPage = false, 42 | showPreviousPage = false; 43 | 44 | if (page < data.page_count){ 45 | showNextPage = true; 46 | } 47 | 48 | if (page > 1 && page <= data.page_count){ 49 | showPreviousPage = true; 50 | } 51 | 52 | res.render('../views/home.handlebars', { 53 | project_name: process.env.PROJECT_DOMAIN, 54 | bot_url: `https://${process.env.PROJECT_DOMAIN}.glitch.me/`, 55 | bot_avatar_url: process.env.BOT_AVATAR_URL, 56 | bot_username: process.env.BOT_USERNAME, 57 | bot_description: process.env.BOT_DESCRIPTION, 58 | page_title: process.env.BOT_NAME, 59 | page_description: process.env.BOT_DESCRIPTION, 60 | is_admin: req.session.is_admin, 61 | post_count: data.post_count, 62 | page_count: data.page_count, 63 | posts: data.posts, 64 | has_posts: !noPosts, 65 | no_posts: noPosts, 66 | current_page: page, 67 | show_pagination: data.page_count > 1, 68 | next_page: page + 1, 69 | previous_page: page - 1, 70 | show_next_page: showNextPage, 71 | show_previous_page: showPreviousPage, 72 | show_admin_link: process.env.SHOW_ADMIN_LINK && 73 | (process.env.SHOW_ADMIN_LINK === 'true' || process.env.SHOW_ADMIN_LINK === 'yes' ? true : false) 74 | }); 75 | }); 76 | } 77 | }); 78 | 79 | router.get('/about', (req, res) => { 80 | res.render('../views/about.handlebars', { 81 | project_name: process.env.PROJECT_DOMAIN, 82 | bot_url: `https://${process.env.PROJECT_DOMAIN}.glitch.me/`, 83 | bot_avatar_url: process.env.BOT_AVATAR_URL, 84 | bot_username: process.env.BOT_USERNAME, 85 | bot_description: process.env.BOT_DESCRIPTION, 86 | page_title: process.env.BOT_NAME, 87 | page_description: process.env.BOT_DESCRIPTION 88 | }); 89 | }); 90 | 91 | router.get('/id.pub', (req, res) => { 92 | let publicKey = fs.readFileSync(publicKeyPath, 'utf8'); 93 | 94 | fs.readFile(publicKeyPath, 'utf8', (err, contents) => { 95 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 96 | res.write(contents); 97 | res.end(); 98 | }); 99 | 100 | }); 101 | 102 | module.exports = router; 103 | -------------------------------------------------------------------------------- /helpers/general.js: -------------------------------------------------------------------------------- 1 | if ( typeof module !== 'undefined' ){ 2 | const fs = require( 'fs' ), 3 | path = require( 'path' ), 4 | request = require( 'request' ); 5 | } 6 | 7 | const helpers = { 8 | getTimestamp: function(){ 9 | return Math.round( ( new Date() ).getTime() / 1000 ); 10 | }, 11 | randomFromArray: function( arr ){ 12 | return arr[Math.floor( Math.random()*arr.length )]; 13 | }, 14 | getRandomInt: function( min, max ){ 15 | return Math.floor( Math.random() * ( max - min + 1 ) ) + min; 16 | }, 17 | getRandomRange: function( min, max, fixed ){ 18 | return ( Math.random() * ( max - min ) + min ).toFixed( fixed ) * 1; 19 | }, 20 | getRandomHex: function(){ 21 | return '#' + Math.random().toString( 16 ).slice( 2, 8 ).toUpperCase(); 22 | }, 23 | shadeColor: function( color, percent ){ 24 | // https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors 25 | let f = parseInt( color.slice( 1 ),16 ),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF; 26 | return `#${( 0x1000000+( Math.round( ( t-R )*p )+R )*0x10000+( Math.round( ( t-G )*p )+G )*0x100+( Math.round( ( t-B )*p )+B ) ).toString( 16 ).slice( 1 )}`; 27 | }, 28 | loadImageAssets: function( cb ){ 29 | /* Load images from the assets folder */ 30 | console.log( 'reading assets folder...' ) 31 | let that = this; 32 | fs.readFile( './.glitch-assets', 'utf8', function ( err, data ){ 33 | if ( err ){ 34 | console.log( 'error:', err ); 35 | return false; 36 | } 37 | data = data.split( '\n' ); 38 | let data_json = JSON.parse( '[' + data.join( ',' ).slice( 0, -1 ) + ']' ), 39 | deleted_images = data_json.reduce( function( filtered, data_img ){ 40 | if ( data_img.deleted ){ 41 | let someNewValue = { name: data_img.name, newProperty: 'Foo' } 42 | filtered.push( data_img.uuid ); 43 | } 44 | return filtered; 45 | }, [] ), 46 | img_urls = []; 47 | 48 | for ( let i = 0, j = data.length; i < j; i++ ){ 49 | if ( data[i].length ){ 50 | let img_data = JSON.parse( data[i] ), 51 | image_url = img_data.url; 52 | 53 | if ( image_url && deleted_images.indexOf( img_data.uuid ) === -1 && that.extension_check( image_url ) ){ 54 | let file_name = that.get_filename_from_url( image_url ).split( '%2F' )[1]; 55 | // console.log( `- ${file_name}` ); 56 | img_urls.push( image_url ); 57 | } 58 | } 59 | } 60 | cb( null, img_urls ); 61 | } ); 62 | }, 63 | extensionCheck: function( url ){ 64 | let file_extension = path.extname( url ).toLowerCase(), 65 | extensions = ['.png', '.jpg', '.jpeg', '.gif']; 66 | return extensions.indexOf( file_extension ) !== -1; 67 | }, 68 | getFilenameFromUrl: function( url ){ 69 | return url.substring( url.lastIndexOf( '/' ) + 1 ); 70 | }, 71 | loadImage: function( url, cb ){ 72 | console.log( `loading remote image: ${url} ...` ); 73 | request( {url: url, encoding: null}, function ( err, res, body ){ 74 | if ( !err && res.statusCode == 200 ){ 75 | let b64content = 'data:' + res.headers['content-type'] + ';base64,'; 76 | console.log( 'image loaded...' ); 77 | cb( null, body.toString( 'base64' ) ); 78 | } else { 79 | console.log( 'ERROR:', err ); 80 | cb( err ); 81 | } 82 | } ); 83 | }, 84 | downloadFile: function( uri, filename, cb ){ 85 | request.head( uri, function( err, res, body ){ 86 | request( uri ).pipe( fs.createWriteStream( filename ) ).on( 'close', cb ); 87 | } ); 88 | } 89 | }; 90 | 91 | if ( typeof module !== 'undefined' ){ 92 | /* This is to make the file usable both in node and on the front end. */ 93 | module.exports = helpers; 94 | } 95 | -------------------------------------------------------------------------------- /generators/joy-division.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ), 2 | crypto = require('crypto'), 3 | Canvas = require( 'canvas' ), 4 | GIFEncoder = require( 'gifencoder' ), 5 | concat = require( 'concat-stream' ), 6 | helpers = require(__dirname + '/../helpers/general.js'), 7 | imgFolder = './.data/img'; 8 | 9 | const filePath = `${ imgFolder }/${ helpers.getTimestamp() }-${ crypto.randomBytes( 4 ).toString( 'hex' ) }`, 10 | filePathGIF = `${ filePath }.gif`, 11 | filePathPNG = `${ filePath }.png`, 12 | fileUrl = `https://${ process.env.PROJECT_DOMAIN }.glitch.me/${ filePath.replace( './.data/', '' ) }`, 13 | fileUrlGIF = `${ fileUrl }.gif`, 14 | fileUrlPNG = `${ fileUrl }.png`; 15 | 16 | module.exports = function( options, cb ) { 17 | /* 18 | Based on http://generativeartistry.com/tutorials/joy-division/ 19 | */ 20 | console.log( 'making waves...' ); 21 | let width = options.width || 640, 22 | height = options.height || 480, 23 | colors = options.colors || ['000', 'fff'], 24 | canvas = Canvas.createCanvas( width, height ), 25 | ctx = canvas.getContext( '2d' ), 26 | encoder; 27 | 28 | if ( options.animate ){ 29 | encoder = new GIFEncoder( width, height ); 30 | 31 | 32 | if ( options.save ){ 33 | encoder.createReadStream().pipe( fs.createWriteStream( filePathGIF ) ); 34 | } else { 35 | encoder.createReadStream().pipe( concat( ( data ) => { 36 | if ( cb ){ 37 | cb( null, data.toString( 'base64' ) ); 38 | } 39 | } ) ); 40 | } 41 | 42 | encoder.start(); 43 | encoder.setRepeat( 0 ); // 0 for repeat, -1 for no-repeat 44 | encoder.setDelay( 100 ); // frame delay in milliseconds 45 | encoder.setQuality( 10 ); // image quality, 10 is default. 46 | } 47 | 48 | ctx.lineWidth = helpers.getRandomInt( 1,4 ); 49 | ctx.fillStyle = colors[0]; 50 | ctx.strokeStyle = colors[1]; 51 | ctx.fillRect( 0, 0, canvas.width, canvas.height ); 52 | 53 | if ( options.animate ){ 54 | encoder.addFrame( ctx ); 55 | } 56 | 57 | let step = helpers.getRandomInt( 8, 12 ); 58 | let lines = []; 59 | 60 | // Create the lines 61 | for ( let i = step; i <= height - step; i += step ) { 62 | 63 | let line = []; 64 | for ( let j = step; j <= height - step; j+= step ) { 65 | let distanceToCenter = Math.abs( j - height / 2 ); 66 | let variance = Math.max( height / 2 - 50 - distanceToCenter, 0 ); 67 | let random = Math.random() * variance / 2 * -1; 68 | let point = { x: j+width/2-height/2, y: i + random }; 69 | line.push( point ) 70 | } 71 | lines.push( line ); 72 | } 73 | 74 | // Do the drawing 75 | for ( let i = 0; i < lines.length; i++ ) { 76 | 77 | ctx.beginPath(); 78 | ctx.moveTo( lines[i][0].x, lines[i][0].y ) 79 | for ( var j = 0; j < lines[i].length - 2; j++ ) { 80 | let xc = ( lines[i][j].x + lines[i][j + 1].x ) / 2; 81 | let yc = ( lines[i][j].y + lines[i][j + 1].y ) / 2; 82 | ctx.quadraticCurveTo( lines[i][j].x, lines[i][j].y, xc, yc ); 83 | } 84 | 85 | ctx.quadraticCurveTo( lines[i][j].x, lines[i][j].y, lines[i][j + 1].x, lines[i][j + 1].y ); 86 | ctx.fill(); 87 | 88 | ctx.stroke(); 89 | if ( options.animate ){ 90 | encoder.addFrame( ctx ); 91 | } 92 | } 93 | 94 | if ( options.animate ){ 95 | encoder.setDelay( 2000 ); 96 | encoder.addFrame( ctx ); 97 | encoder.finish(); 98 | if ( cb ){ 99 | cb( null, fileUrlGIF ); 100 | 101 | } 102 | } 103 | else{ 104 | if ( options.save ){ 105 | const out = fs.createWriteStream( filePathPNG ), 106 | stream = canvas.createPNGStream(); 107 | 108 | stream.pipe( out ); 109 | 110 | out.on( 'finish', function(){ 111 | if ( cb ){ 112 | cb( null, fileUrlPNG ); 113 | } 114 | } ); 115 | } else { 116 | cb( null, canvas.toBuffer().toString( 'base64' ) ); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /routes/feed.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | router = express.Router(), 3 | RSS = require('rss'), 4 | moment = require('moment'), 5 | dbHelper = require(__dirname + '/../helpers/db.js'); 6 | 7 | router.get('/', (req, res) => { 8 | let xml = ''; 9 | 10 | console.log('rendering feed...'); 11 | dbHelper.getPosts(null, (err, data) => { 12 | console.log(err, data); 13 | if (data && data.posts){ 14 | xml = ` 15 | 16 | https://${process.env.PROJECT_DOMAIN}.glitch.me/feed 17 | ${process.env.BOT_NAME} 18 | ${process.env.BOT_DESCRIPTION} 19 | 2018-09-14T18:15:10Z 20 | ${process.env.BOT_AVATAR_URL} 21 | 22 | https://${process.env.PROJECT_DOMAIN}.glitch.me 23 | http://activitystrea.ms/schema/1.0/service 24 | https://${process.env.PROJECT_DOMAIN}.glitch.me 25 | ${process.env.BOT_USERNAME} 26 | bot@${process.env.PROJECT_DOMAIN}.glitch.me 27 | <p>${process.env.BOT_DESCRIPTION}</p> 28 | 29 | 30 | 31 | ${process.env.BOT_USERNAME} 32 | ${process.env.BOT_NAME} 33 | ${process.env.BOT_DESCRIPTION} 34 | public 35 | 36 | 37 | 38 | 39 | 40 | `; 41 | 42 | data.posts.forEach((post) => { 43 | // post.date_formatted = moment(post.date).fromNow(); 44 | xml += ` 45 | https://${process.env.PROJECT_DOMAIN}.glitch.me/post/${post.id} 46 | ${moment(post.date)} 47 | ${moment(post.date)} 48 | New status by ${process.env.BOT_USERNAME} 49 | http://activitystrea.ms/schema/1.0/comment 50 | http://activitystrea.ms/schema/1.0/post 51 | 52 | <p><span class="h-card"><a href="https://mastodon.social/@metalbob" class="u-url mention">@<span>metalbob</span></a></span> i suppose it&apos;s possible although hopefully it never becomes actual law</p> 53 | 54 | 55 | public 56 | 57 | `; 58 | }); 59 | 60 | xml += '\n'; 61 | } 62 | 63 | res.setHeader('Content-Type', 'application/rss+xml'); 64 | // res.send(feed.xml({indent: true})); 65 | res.send(xml); 66 | }); 67 | }); 68 | 69 | module.exports = router; 70 | -------------------------------------------------------------------------------- /helpers/colorbrewer.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | //Green to yellow 3 | ["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"].reverse(), 4 | //Blue-green-yellow 5 | ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"].reverse(), 6 | //Blue to green 7 | ["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"].reverse(), 8 | //Green to blue 9 | ["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"].reverse(), 10 | //Green-blue-purple 11 | ["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"].reverse(), 12 | //Blue to purple 13 | ["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"].reverse(), 14 | //Purple to blue 15 | ["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"].reverse(), 16 | //Purple to red 17 | ["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"].reverse(), 18 | //Red to purple 19 | ["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"].reverse(), 20 | //Red to orange 21 | ["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"].reverse(), 22 | //Red-orange-yellow 23 | ["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"].reverse(), 24 | //Brown-orange-yellow 25 | ["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"].reverse(), 26 | //Purples 27 | ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"].reverse(), 28 | //Blues 29 | ["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"].reverse(), 30 | //Greens 31 | ["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"].reverse(), 32 | //Oranges 33 | ["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"].reverse(), 34 | //Reds 35 | ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"].reverse(), 36 | //Grays 37 | ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"].reverse(), 38 | //Purple to orange 39 | ["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"].reverse(), 40 | //Green to brown 41 | ["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"].reverse(), 42 | //Purple to green 43 | ["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"].reverse(), 44 | //Purple to light green 45 | ["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"].reverse(), 46 | //Blue to red 47 | ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"].reverse(), 48 | //Gray to red 49 | ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"].reverse(), 50 | //Blue-yellow-red 51 | ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"].reverse(), 52 | //Spectral 53 | ["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"].reverse(), 54 | //Green-yellow-red 55 | ["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"].reverse(), 56 | //Accent 57 | ["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"].reverse(), 58 | //Dark 59 | ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"].reverse(), 60 | //Paired 61 | ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"].reverse(), 62 | //Pastel 1 63 | ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"].reverse(), 64 | //Pastel 2 65 | ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"].reverse(), 66 | //Set 1 67 | ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"].reverse(), 68 | //Set 2 69 | ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"].reverse(), 70 | //Set 3 71 | ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"].reverse() 72 | ]; -------------------------------------------------------------------------------- /.glitch-assets: -------------------------------------------------------------------------------- 1 | {"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"} 2 | {"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"} 3 | {"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"} 4 | {"uuid":"adSBq97hhhpFNUna","deleted":true} 5 | {"uuid":"adSBq97hhhpFNUnb","deleted":true} 6 | {"uuid":"adSBq97hhhpFNUnc","deleted":true} 7 | {"name":"null.png","date":"2018-09-13T21:12:59.652Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fnull.png","type":"image/png","size":5255,"imageWidth":128,"imageHeight":128,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fnull.png","thumbnailWidth":128,"thumbnailHeight":128,"dominantColor":"rgb(76,172,220)","uuid":"y4bX37K73cQTbTjE"} 8 | {"uuid":"y4bX37K73cQTbTjE","deleted":true} 9 | {"name":"smiling-face-with-smiling-eyes.png","date":"2018-09-27T21:38:47.331Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fsmiling-face-with-smiling-eyes.png","type":"image/png","size":31773,"imageWidth":400,"imageHeight":400,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fsmiling-face-with-smiling-eyes.png","thumbnailWidth":330,"thumbnailHeight":330,"dominantColor":null,"uuid":"lmZRNK6DdCO5aJr7"} 10 | {"name":"glitch-fediverse-bot.png","date":"2018-09-29T12:44:17.606Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot.png","type":"image/png","size":55034,"imageWidth":1920,"imageHeight":1079,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot.png","thumbnailWidth":330,"thumbnailHeight":186,"dominantColor":"rgb(252,252,252)","uuid":"4Jq94M3MUTFcsK6b"} 11 | {"uuid":"4Jq94M3MUTFcsK6b","deleted":true} 12 | {"name":"glitch-fediverse-bot-small-1024px.png","date":"2018-09-29T12:49:07.895Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot-small-1024px.png","type":"image/png","size":60397,"imageWidth":1024,"imageHeight":509,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot-small-1024px.png","thumbnailWidth":330,"thumbnailHeight":165,"dominantColor":"rgb(252,252,252)","uuid":"iLCHg6pDTsAX7jkg"} 13 | {"name":"glitch-fediverse-bot.png","date":"2018-09-29T12:49:07.963Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot.png","type":"image/png","size":58653,"imageWidth":1920,"imageHeight":1079,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot.png","thumbnailWidth":330,"thumbnailHeight":186,"dominantColor":"rgb(252,252,252)","uuid":"0KNy0WXAgI1pZ0fd"} 14 | {"name":"glitch-fediverse-bot-960px.png","date":"2018-09-29T12:49:08.030Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot-960px.png","type":"image/png","size":50867,"imageWidth":960,"imageHeight":540,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot-960px.png","thumbnailWidth":330,"thumbnailHeight":186,"dominantColor":"rgb(252,252,252)","uuid":"8tHyMsnvvVKkjFXF"} 15 | {"name":"glitch-fediverse-bot-with-image.png","date":"2018-10-03T21:11:53.744Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot-with-image.png","type":"image/png","size":469823,"imageWidth":2880,"imageHeight":1465,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot-with-image.png","thumbnailWidth":330,"thumbnailHeight":168,"dominantColor":"rgb(252,252,252)","uuid":"oHDFFpP6XyCauPmh"} 16 | {"name":"bot-replies.png","date":"2018-10-09T12:35:59.939Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fbot-replies.png","type":"image/png","size":186912,"imageWidth":2834,"imageHeight":1256,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fbot-replies.png","thumbnailWidth":330,"thumbnailHeight":147,"dominantColor":"rgb(236,244,244)","uuid":"sHZ0AbdbgCtnaQYM"} 17 | {"uuid":"iLCHg6pDTsAX7jkg","deleted":true} 18 | {"uuid":"0KNy0WXAgI1pZ0fd","deleted":true} 19 | {"uuid":"8tHyMsnvvVKkjFXF","deleted":true} 20 | {"name":"glitch-fediverse-bot.png","date":"2018-10-09T12:49:00.686Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fglitch-fediverse-bot.png","type":"image/png","size":206001,"imageWidth":2854,"imageHeight":1398,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Fglitch-fediverse-bot.png","thumbnailWidth":330,"thumbnailHeight":162,"dominantColor":"rgb(236,244,244)","uuid":"6oFnGGQy88GLx9Be"} 21 | {"name":"relieved-face.png","date":"2020-01-21T16:55:20.629Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Frelieved-face.png","type":"image/png","size":4782,"imageWidth":512,"imageHeight":512,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Frelieved-face.png","thumbnailWidth":330,"thumbnailHeight":330,"uuid":"ystBbCRjHvMTAkmP"} 22 | {"uuid":"lmZRNK6DdCO5aJr7","deleted":true} 23 | {"name":"feed.png","date":"2020-01-21T20:46:04.612Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Ffeed.png","type":"image/png","size":435181,"imageWidth":2822,"imageHeight":1630,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Ffeed.png","thumbnailWidth":330,"thumbnailHeight":191,"uuid":"QJrzlYrlyc1CjcsQ"} 24 | {"name":"feed-mastodon.png","date":"2020-01-21T20:46:04.659Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Ffeed-mastodon.png","type":"image/png","size":235591,"imageWidth":644,"imageHeight":1478,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Ffeed-mastodon.png","thumbnailWidth":144,"thumbnailHeight":330,"uuid":"i1w6Ti1cNwHF1vbT"} 25 | {"name":"feed-mastodon-small.png","date":"2020-01-21T20:48:45.012Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Ffeed-mastodon-small.png","type":"image/png","size":82308,"imageWidth":200,"imageHeight":459,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Ffeed-mastodon-small.png","thumbnailWidth":144,"thumbnailHeight":330,"uuid":"x1IOEKWrW0Uaafk4"} 26 | {"name":"feed-comb.png","date":"2020-01-21T21:08:53.717Z","url":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Ffeed-comb.png","type":"image/png","size":453422,"imageWidth":2156,"imageHeight":1630,"thumbnail":"https://cdn.glitch.com/a4825d5c-d1d6-4780-8464-8636780177ef%2Fthumbnails%2Ffeed-comb.png","thumbnailWidth":330,"thumbnailHeight":250,"uuid":"YRb0rstRrVoXfWQv"} 27 | -------------------------------------------------------------------------------- /helpers/db.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ), 2 | dbFile = './.data/sqlite.db', 3 | exists = fs.existsSync( dbFile ), 4 | sqlite3 = require( 'sqlite3' ).verbose(), 5 | sqlDb = new sqlite3.Database( dbFile ), 6 | POSTS_PER_PAGE = process.env.POSTS_PER_PAGE || 5; 7 | 8 | /* 9 | 10 | Posts table 11 | 12 | id INT NOT NULL AUTO_INCREMENT 13 | date DATETIME DEFAULT current_timestamp 14 | type VARCHAR( 255 ) 15 | in_reply_to TEXT 16 | content TEXT 17 | attachment TEXT [this is a stringified JSON, see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment] 18 | 19 | Followers table 20 | 21 | url TEXT PRIMARY KEY 22 | date DATETIME DEFAULT current_timestamp 23 | 24 | Events table 25 | 26 | id TEXT PRIMARY 27 | date DATETIME DEFAULT current_timestamp 28 | 29 | 30 | */ 31 | 32 | module.exports = { 33 | init: function( cb ){ 34 | sqlDb.serialize( function(){ 35 | /* 36 | TODO: Rewrite this with promises and callback support. 37 | */ 38 | 39 | sqlDb.run( 'CREATE TABLE IF NOT EXISTS Posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, date DATETIME DEFAULT current_timestamp, type VARCHAR( 255 ), in_reply_to TEXT, content TEXT, attachment TEXT )', function( err, data ){ 40 | if ( err ){ 41 | console.log( err ); 42 | } 43 | } ); 44 | 45 | sqlDb.run( 'CREATE TABLE IF NOT EXISTS Followers ( url TEXT PRIMARY KEY, date DATETIME DEFAULT current_timestamp )', function( err, data ){ 46 | if ( err ){ 47 | console.log( err ); 48 | } 49 | } ); 50 | 51 | sqlDb.run( 'CREATE TABLE IF NOT EXISTS Events ( id TEXT PRIMARY KEY, date DATETIME DEFAULT current_timestamp )', function( err, data ){ 52 | if ( err ){ 53 | console.log( err ); 54 | } 55 | } ); 56 | 57 | } ); 58 | }, 59 | getPosts: function( options, cb ){ 60 | let data = [], 61 | page = ( options && options.page ? options.page : 1 ), 62 | queryLimit = POSTS_PER_PAGE, 63 | offset = POSTS_PER_PAGE * ( page - 1 ); 64 | 65 | if (options && typeof options.limit !== 'undefined') { 66 | queryLimit = parseInt( options.limit ); 67 | } 68 | 69 | sqlDb.serialize( function(){ 70 | /* 71 | let dbQuery = `SELECT *, COUNT( * ) AS total_count from Posts ORDER BY date DESC LIMIT ${POSTS_PER_PAGE} OFFSET ${offset}`; 72 | This query doesn't seem to work, two DB calls are necessary to get the total post count. 73 | 74 | */ 75 | 76 | sqlDb.all( 'SELECT COUNT( * ) AS total_count FROM Posts', function( err, rows ) { 77 | let totalCount = 0; 78 | 79 | if ( rows ){ 80 | totalCount = rows[0].total_count; 81 | } 82 | 83 | let totalPages = Math.ceil( totalCount/POSTS_PER_PAGE ); 84 | let dbQuery = `SELECT * from Posts ORDER BY date DESC${ queryLimit > 0 ? ` LIMIT ${queryLimit} OFFSET ${offset} ` : '' }`; 85 | 86 | sqlDb.all( dbQuery, function( err, rows ) { 87 | if ( cb ){ 88 | let db_return = { 89 | post_count: totalCount, 90 | page_count: totalPages, 91 | posts: rows 92 | }; 93 | cb( err, db_return ); 94 | } 95 | } ); 96 | 97 | } ); 98 | } ); 99 | }, 100 | getPost: function( post_id, cb ){ 101 | let data = []; 102 | sqlDb.serialize( function(){ 103 | sqlDb.all( `SELECT * from Posts WHERE id=${post_id}`, function( err, rows ) { 104 | if ( cb ){ 105 | let post_data = ( rows ? rows[0] : null ); 106 | cb( err, post_data ); 107 | } 108 | } ); 109 | } ); 110 | }, 111 | savePost: function( post_data, cb ){ 112 | let post_type = post_data.type || 'Note', 113 | post_content = post_data.content || '', 114 | in_reply_to = post_data.in_reply_to || '', 115 | post_attachment = post_data.attachment.toString() || '[]'; 116 | 117 | sqlDb.serialize( function() { 118 | // sqlDb.run( `INSERT INTO Posts ( type, content, attachment ) VALUES ( "${post_type}", "${post_content}", "${post_attachment}" )`, function( err, data ){ 119 | sqlDb.run( `INSERT INTO Posts ( type, content, in_reply_to, attachment ) VALUES ( '${post_type}', '${post_content}', '${in_reply_to}', '${post_attachment}' )`, function( err, data ){ 120 | if ( err ){ 121 | console.log( err ); 122 | } 123 | if ( cb ){ 124 | cb( err, this ); 125 | } 126 | } ); 127 | } ); 128 | }, 129 | deletePost: function( post_id, bot, cb ){ 130 | const dbHelper = this; 131 | 132 | sqlDb.serialize( function() { 133 | sqlDb.run( `DELETE FROM Posts WHERE id="${post_id}"`, function( err, data ){ 134 | dbHelper.getFollowers( function( err, followers ){ 135 | console.log( 'followers:', followers ); 136 | followers.forEach( function( follower ){ 137 | if ( follower.url ){ 138 | bot.deletePost( post_id, follower.url, function( err, data ){ 139 | if ( cb ){ 140 | cb( err, this ); 141 | } 142 | } ); 143 | } 144 | } ); 145 | } ); 146 | } ); 147 | } ); 148 | }, 149 | saveFollower: function( payload, cb ){ 150 | sqlDb.serialize( function() { 151 | sqlDb.run( `INSERT INTO Followers ( url ) VALUES ( "${payload.actor}" )`, function( err, data ){ 152 | if ( cb ){ 153 | cb( err, this ); 154 | } 155 | } ); 156 | } ); 157 | }, 158 | removeFollower: function( payload, cb ){ 159 | sqlDb.serialize( function() { 160 | sqlDb.run( `DELETE FROM Followers WHERE url="${payload.actor}"`, function( err, data ){ 161 | if ( cb ){ 162 | cb( err, this ); 163 | } 164 | } ); 165 | } ); 166 | }, 167 | getFollowers: function( cb ){ 168 | let data = []; 169 | sqlDb.serialize( function(){ 170 | 171 | sqlDb.all( "SELECT * from Followers ORDER BY date DESC", function( err, rows ) { 172 | if ( cb ){ 173 | cb( err, rows ); 174 | } 175 | } ); 176 | } ); 177 | }, 178 | saveEvent: function( event_id, cb ){ 179 | sqlDb.serialize( function() { 180 | sqlDb.run( `INSERT INTO Events ( id ) VALUES ( '${event_id}' )`, function( err, data ){ 181 | if ( err ){ 182 | console.log( err ); 183 | } 184 | if ( cb ){ 185 | cb( err, this ); 186 | } 187 | } ); 188 | } ); 189 | }, 190 | getEvent: function( event_id, cb ){ 191 | let data = []; 192 | sqlDb.serialize( function(){ 193 | sqlDb.all( `SELECT * from Events WHERE id='${event_id}'`, function( err, rows ) { 194 | if ( cb ){ 195 | let data = ( rows ? rows[0] : null ); 196 | cb( err, data ); 197 | } 198 | } ); 199 | } ); 200 | }, 201 | getEvents: function( cb ){ 202 | sqlDb.serialize( function(){ 203 | sqlDb.all( 'SELECT * FROM Events', function( err, rows ) { 204 | if ( cb ){ 205 | cb( err, rows ); 206 | } 207 | } ); 208 | } ); 209 | }, 210 | getReplies: function( in_reply_to, cb ){ 211 | let data = []; 212 | sqlDb.serialize( function(){ 213 | sqlDb.all( `SELECT * from Posts WHERE in_reply_to='${in_reply_to}'`, function( err, rows ) { 214 | if ( cb ){ 215 | let post_data = ( rows ? rows[0] : null ); 216 | cb( err, post_data ); 217 | } 218 | } ); 219 | } ); 220 | }, 221 | dropTable: function( table, cb ){ 222 | sqlDb.serialize( function(){ 223 | if ( table && exists ) { 224 | sqlDb.run( `DROP TABLE ${table};` ); 225 | console.log( `dropped table ${table}...` ); 226 | if ( cb ){ 227 | cb( null ); 228 | } 229 | } 230 | else { 231 | console.log( 'table not found...' ); 232 | if ( cb ){ 233 | cb( null ); 234 | } 235 | } 236 | } ); 237 | } 238 | }; 239 | 240 | -------------------------------------------------------------------------------- /bot/bot.js: -------------------------------------------------------------------------------- 1 | if (!process.env.PROJECT_NAME || !process.env.PROJECT_ID){ 2 | require('dotenv').config(); 3 | } 4 | 5 | const fs = require('fs'), 6 | crypto = require('crypto'), 7 | url = require('url'), 8 | util = require('util'), 9 | moment = require('moment'), 10 | dbHelper = require(__dirname + '/../helpers/db.js'), 11 | keys = require(__dirname + '/../helpers/keys.js'), 12 | request = require('request'), 13 | publicKeyPath = '.data/rsa/pubKey', 14 | privateKeyPath = '.data/rsa/privKey', 15 | botUrl = `https://${ process.env.PROJECT_DOMAIN}.glitch.me`, 16 | botComposeReply = require(__dirname + '/responses.js'); 17 | 18 | if (!fs.existsSync(publicKeyPath) || !fs.existsSync(privateKeyPath)) { 19 | keys.generateKeys(() => { 20 | process.kill(process.pid); 21 | }); 22 | } 23 | else{ 24 | const publicKey = fs.readFileSync(publicKeyPath, 'utf8'), 25 | privateKey = fs.readFileSync(privateKeyPath, 'utf8'); 26 | 27 | module.exports = { 28 | bot_url: botUrl, 29 | links: [ 30 | // { 31 | // rel: 'http://webfinger.net/rel/profile-page', 32 | // type: 'text/html', 33 | // href: `${ botUrl }` 34 | // }, 35 | // { 36 | // rel: 'http://schemas.google.com/g/2010#updates-from', 37 | // type: 'application/atom+xml', 38 | // href: `${ botUrl }/feed` 39 | // }, 40 | { 41 | rel: 'self', 42 | type: 'application/activity+json', 43 | href: `${ botUrl }/bot` 44 | }, 45 | // { 46 | // rel: 'hub', 47 | // href: `${ botUrl }/pubsub` 48 | // }, 49 | // { 50 | // rel: 'salmon', 51 | // href: `${ botUrl }/salmon` 52 | // }, 53 | // { 54 | // rel: 'magic-public-key', 55 | // href: `data:application/magic-public-key,RSA.${ publicKey.replace('-----BEGIN PUBLIC KEY-----\n', '').replace('\n-----END PUBLIC KEY-----', '').replace('\\n', '') }` 56 | // } 57 | ], 58 | info: { 59 | '@context': [ 60 | 'https://www.w3.org/ns/activitystreams', 61 | 'https://w3id.org/security/v1' 62 | ], 63 | 'id': `${ botUrl }/bot`, 64 | 'icon': [{ 65 | 'url': process.env.BOT_AVATAR_URL, 66 | 'type': 'Image' 67 | }], 68 | 'image': [{ 69 | 'url': process.env.BOT_AVATAR_URL, 70 | 'type': 'Image' 71 | }], 72 | 'type': 'Service', 73 | 'name': process.env.BOT_NAME, 74 | 'preferredUsername': process.env.BOT_USERNAME, 75 | 'inbox': `${ botUrl }/inbox`, 76 | 'publicKey': { 77 | 'id': `${ botUrl }/bot#main-key`, 78 | 'owner': `${ botUrl }/bot`, 79 | 'publicKeyPem': publicKey 80 | } 81 | }, 82 | script: require(__dirname + '/script.js'), 83 | composeReply: botComposeReply, 84 | sendReply: (options, cb) => { 85 | let bot = this, 86 | replyToUsername = ''; 87 | 88 | try{ 89 | let actorUrlParts = options.payload.actor.split('/'); 90 | let username = actorUrlParts[actorUrlParts.length-1]; 91 | replyToUsername = `@${ username}@${ url.parse(options.payload.actor).hostname} `; 92 | 93 | console.log({ replyToUsername }); 94 | } catch(err){ /* noop */ } 95 | 96 | bot.createPost({ 97 | type: 'Note', 98 | content: `
${ options.payload.object.content }${ options.payload.object.url }

${ options.reply_message }

`, 99 | reply_message: `${ replyToUsername } ${ options.reply_message }`, 100 | in_reply_to: options.payload.object.url 101 | }, (err, message) => { 102 | // console.log(err, message); 103 | }); 104 | }, 105 | createPost: (options, cb) => { 106 | let bot = this; 107 | 108 | if ((!options.content || options.content.trim().length === 0) && !options.attachment){ 109 | console.log('error: no post content or attachments'); 110 | return false; 111 | } 112 | 113 | let postType = options.type || 'Note', 114 | postDescription = options.description, 115 | postDate = moment().format(), 116 | postInReplyTo = options.in_reply_to || null, 117 | replyMessage = options.reply_message || null, 118 | postContent = options.content || options.url || '', 119 | postAttachment = JSON.stringify(options.attachment) || '[]'; 120 | 121 | dbHelper.savePost({ 122 | type: postType, 123 | content: postContent, 124 | attachment: postAttachment 125 | }, (err, data) => { 126 | let postID = data.lastID; 127 | 128 | let postObject; 129 | 130 | if (postType === 'Note'){ 131 | postObject = { 132 | 'id': `${ botUrl }/post/${ postID }`, 133 | 'type': postType, 134 | 'published': postDate, 135 | 'attributedTo': `${ botUrl }/bot`, 136 | 'content': replyMessage || postContent, 137 | 'to': 'https://www.w3.org/ns/activitystreams#Public' 138 | }; 139 | 140 | if (options.attachment){ 141 | let attachments = []; 142 | 143 | options.attachment.forEach((attachment) => { 144 | attachments.push({ 145 | 'type': 'Image', 146 | 'content': attachment.content, 147 | 'url': attachment.url 148 | }); 149 | }); 150 | postObject.attachment = attachments; 151 | } 152 | } 153 | 154 | if (postInReplyTo){ 155 | postObject.inReplyTo = postInReplyTo; 156 | } 157 | 158 | let post = { 159 | '@context': 'https://www.w3.org/ns/activitystreams', 160 | 'id': `${ botUrl }/post/${ postID }`, 161 | 'type': 'Create', 162 | 'actor': `${ botUrl }/bot`, 163 | 'object': postObject 164 | } 165 | 166 | console.log({postInReplyTo}); 167 | 168 | dbHelper.getFollowers((err, followers) => { 169 | if (followers){ 170 | console.log(`sending update to ${ followers.length} follower(s)...`); 171 | 172 | followers.forEach((follower) => { 173 | if (follower.url){ 174 | bot.signAndSend({ 175 | follower: follower, 176 | message: post 177 | }, (err, data) => { 178 | 179 | }); 180 | } 181 | }); 182 | } 183 | }); 184 | 185 | if (cb){ 186 | cb(null, post); 187 | } 188 | }); 189 | }, 190 | deletePost: (postID, followerUrl, cb) => { 191 | let bot = this; 192 | // guid = crypto.randomBytes(16).toString('hex'); 193 | 194 | bot.signAndSend({ 195 | follower: { 196 | url: followerUrl 197 | }, 198 | message: { 199 | '@context': 'https://www.w3.org/ns/activitystreams', 200 | // 'summary': `${ bot} deleted a post`, 201 | // 'id': `${ bot.bot_url }/${ guid }`, 202 | 'type': 'Delete', 203 | 'actor': `${ bot.bot_url }/bot`, 204 | 'object': `${ bot.bot_url }/post/${ postID }` 205 | } 206 | }, (err, data) => { 207 | if (cb){ 208 | cb(err, data); 209 | } 210 | }); 211 | }, 212 | accept: (payload, cb) => { 213 | let bot = this, 214 | guid = crypto.randomBytes(16).toString('hex'); 215 | 216 | dbHelper.getEvent(payload.id, (err, eventData) => { 217 | console.log('getEvent', err, eventData); 218 | 219 | console.log(bot); 220 | 221 | bot.signAndSend({ 222 | follower: { 223 | url: payload.actor 224 | }, 225 | message: { 226 | '@context': 'https://www.w3.org/ns/activitystreams', 227 | 'id': `${ bot.bot_url }/${ guid }`, 228 | 'type': 'Accept', 229 | 'actor': `${ bot.bot_url }/bot`, 230 | 'object': payload, 231 | } 232 | }, (err, data) => { 233 | if (eventData){ 234 | err = 'duplicate event'; 235 | console.log('error: duplicate event'); 236 | } else { 237 | console.log('saving event', payload.id); 238 | dbHelper.saveEvent(payload.id); 239 | } 240 | 241 | if (cb){ 242 | cb(err, payload, data); 243 | } 244 | }); 245 | 246 | }); 247 | // dbHelper.getEvent(payload.id, (err, data) => { 248 | // console.log('getEvent', err, data); 249 | 250 | // if (!err && !data){ 251 | // bot.signAndSend({ 252 | // follower: { 253 | // url: payload.actor 254 | // }, 255 | // message: { 256 | // '@context': 'https://www.w3.org/ns/activitystreams', 257 | // 'id': `${ bot.bot_url }/${ guid }`, 258 | // 'type': 'Accept', 259 | // 'actor': `${ bot.bot_url }/bot`, 260 | // 'object': payload, 261 | // } 262 | // }, (err, data) => { 263 | // if (cb){ 264 | // cb(err, payload, data); 265 | // } 266 | // console.log('saving event', payload.id) 267 | // dbHelper.saveEvent(payload.id); 268 | // }); 269 | // } else if (!err){ 270 | // console.log('duplicate event'); 271 | // } 272 | // }); 273 | }, 274 | signAndSend: (options, cb) => { 275 | let bot = this; 276 | // console.log('message to sign:'); 277 | // console.log(util.inspect(options.message, false, null, true)); 278 | 279 | // options.follower.url = options.follower.url.replace('http://localhost:3000', 'https://befc66af.ngrok.io'); 280 | 281 | if (options.follower.url && options.follower.url !== 'undefined'){ 282 | options.follower.domain = url.parse(options.follower.url).hostname; 283 | 284 | let signer = crypto.createSign('sha256'), 285 | d = new Date(), 286 | stringToSign = `(request-target): post /inbox\nhost: ${ options.follower.domain}\ndate: ${ d.toUTCString() }`; 287 | 288 | signer.update(stringToSign); 289 | signer.end(); 290 | 291 | let signature = signer.sign(privateKey); 292 | let signatureB64 = signature.toString('base64'); 293 | let header = `keyId="${ botUrl }/bot",headers="(request-target) host date",signature="${ signatureB64}"`; 294 | 295 | let reqObject = { 296 | url: `https://${ options.follower.domain }/inbox`, 297 | headers: { 298 | 'Host': options.follower.domain, 299 | 'Date': d.toUTCString(), 300 | 'Signature': header 301 | }, 302 | method: 'POST', 303 | json: true, 304 | body: options.message 305 | }; 306 | 307 | // console.log('request object:'); 308 | // console.log(util.inspect(reqObject, false, null, true)); 309 | 310 | request(reqObject, (error, response) => { 311 | console.log(`sent message to ${ options.follower.url}...`); 312 | if (error) { 313 | console.log('error:', error, response); 314 | } 315 | else { 316 | console.log('response:', response.statusCode, response.statusMessage); 317 | // console.log(response); 318 | } 319 | 320 | if (cb){ 321 | cb(error, response); 322 | } 323 | }); 324 | } 325 | } 326 | }; 327 | } 328 | -------------------------------------------------------------------------------- /public/libs/bootstrap/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,n){"use strict";function i(t,e){for(var n=0;n0?i:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(n){t(n).trigger(e.end)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var s in n)if(Object.prototype.hasOwnProperty.call(n,s)){var r=n[s],o=e[s],a=o&&i.isElement(o)?"element":(l=o,{}.toString.call(l).match(/\s([a-zA-Z]+)/)[1].toLowerCase());if(!new RegExp(r).test(a))throw new Error(t.toUpperCase()+': Option "'+s+'" provided type "'+a+'" but expected type "'+r+'".')}var l}};return e=("undefined"==typeof window||!window.QUnit)&&{end:"transitionend"},t.fn.emulateTransitionEnd=n,i.supportsTransitionEnd()&&(t.event.special[i.TRANSITION_END]={bindType:e.end,delegateType:e.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}}),i}(e),L=(a="alert",h="."+(l="bs.alert"),c=(o=e).fn[a],u={CLOSE:"close"+h,CLOSED:"closed"+h,CLICK_DATA_API:"click"+h+".data-api"},f="alert",d="fade",_="show",g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.removeData(this._element,l),this._element=null},e._getRootElement=function(t){var e=P.getSelectorFromElement(t),n=!1;return e&&(n=o(e)[0]),n||(n=o(t).closest("."+f)[0]),n},e._triggerCloseEvent=function(t){var e=o.Event(u.CLOSE);return o(t).trigger(e),e},e._removeElement=function(t){var e=this;o(t).removeClass(_),P.supportsTransitionEnd()&&o(t).hasClass(d)?o(t).one(P.TRANSITION_END,function(n){return e._destroyElement(t,n)}).emulateTransitionEnd(150):this._destroyElement(t)},e._destroyElement=function(t){o(t).detach().trigger(u.CLOSED).remove()},t._jQueryInterface=function(e){return this.each(function(){var n=o(this),i=n.data(l);i||(i=new t(this),n.data(l,i)),"close"===e&&i[e](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),o(document).on(u.CLICK_DATA_API,'[data-dismiss="alert"]',g._handleDismiss(new g)),o.fn[a]=g._jQueryInterface,o.fn[a].Constructor=g,o.fn[a].noConflict=function(){return o.fn[a]=c,g._jQueryInterface},g),R=(m="button",E="."+(v="bs.button"),T=".data-api",y=(p=e).fn[m],C="active",I="btn",A="focus",b='[data-toggle^="button"]',D='[data-toggle="buttons"]',S="input",w=".active",N=".btn",O={CLICK_DATA_API:"click"+E+T,FOCUS_BLUR_DATA_API:"focus"+E+T+" blur"+E+T},k=function(){function t(t){this._element=t}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p(this._element).closest(D)[0];if(n){var i=p(this._element).find(S)[0];if(i){if("radio"===i.type)if(i.checked&&p(this._element).hasClass(C))t=!1;else{var s=p(n).find(w)[0];s&&p(s).removeClass(C)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!p(this._element).hasClass(C),p(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!p(this._element).hasClass(C)),t&&p(this._element).toggleClass(C)},e.dispose=function(){p.removeData(this._element,v),this._element=null},t._jQueryInterface=function(e){return this.each(function(){var n=p(this).data(v);n||(n=new t(this),p(this).data(v,n)),"toggle"===e&&n[e]()})},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),p(document).on(O.CLICK_DATA_API,b,function(t){t.preventDefault();var e=t.target;p(e).hasClass(I)||(e=p(e).closest(N)),k._jQueryInterface.call(p(e),"toggle")}).on(O.FOCUS_BLUR_DATA_API,b,function(t){var e=p(t.target).closest(N)[0];p(e).toggleClass(A,/^focus(in)?$/.test(t.type))}),p.fn[m]=k._jQueryInterface,p.fn[m].Constructor=k,p.fn[m].noConflict=function(){return p.fn[m]=y,k._jQueryInterface},k),j=function(t){var e="carousel",n="bs.carousel",i="."+n,o=t.fn[e],a={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},l={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},h="next",c="prev",u="left",f="right",d={SLIDE:"slide"+i,SLID:"slid"+i,KEYDOWN:"keydown"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i,TOUCHEND:"touchend"+i,LOAD_DATA_API:"load"+i+".data-api",CLICK_DATA_API:"click"+i+".data-api"},_="carousel",g="active",p="slide",m="carousel-item-right",v="carousel-item-left",E="carousel-item-next",T="carousel-item-prev",y={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},C=function(){function o(e,n){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(n),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(y.INDICATORS)[0],this._addEventListeners()}var C=o.prototype;return C.next=function(){this._isSliding||this._slide(h)},C.nextWhenVisible=function(){!document.hidden&&t(this._element).is(":visible")&&"hidden"!==t(this._element).css("visibility")&&this.next()},C.prev=function(){this._isSliding||this._slide(c)},C.pause=function(e){e||(this._isPaused=!0),t(this._element).find(y.NEXT_PREV)[0]&&P.supportsTransitionEnd()&&(P.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},C.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},C.to=function(e){var n=this;this._activeElement=t(this._element).find(y.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var s=e>i?h:c;this._slide(s,this._items[e])}},C.dispose=function(){t(this._element).off(i),t.removeData(this._element,n),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},C._getConfig=function(t){return t=r({},a,t),P.typeCheckConfig(e,t,l),t},C._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},C._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},C._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(y.ITEM)),this._items.indexOf(e)},C._getItemByDirection=function(t,e){var n=t===h,i=t===c,s=this._getItemIndex(e),r=this._items.length-1;if((i&&0===s||n&&s===r)&&!this._config.wrap)return e;var o=(s+(t===c?-1:1))%this._items.length;return-1===o?this._items[this._items.length-1]:this._items[o]},C._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),s=this._getItemIndex(t(this._element).find(y.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:s,to:i});return t(this._element).trigger(r),r},C._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(y.ACTIVE).removeClass(g);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(g)}},C._slide=function(e,n){var i,s,r,o=this,a=t(this._element).find(y.ACTIVE_ITEM)[0],l=this._getItemIndex(a),c=n||a&&this._getItemByDirection(e,a),_=this._getItemIndex(c),C=Boolean(this._interval);if(e===h?(i=v,s=E,r=u):(i=m,s=T,r=f),c&&t(c).hasClass(g))this._isSliding=!1;else if(!this._triggerSlideEvent(c,r).isDefaultPrevented()&&a&&c){this._isSliding=!0,C&&this.pause(),this._setActiveIndicatorElement(c);var I=t.Event(d.SLID,{relatedTarget:c,direction:r,from:l,to:_});P.supportsTransitionEnd()&&t(this._element).hasClass(p)?(t(c).addClass(s),P.reflow(c),t(a).addClass(i),t(c).addClass(i),t(a).one(P.TRANSITION_END,function(){t(c).removeClass(i+" "+s).addClass(g),t(a).removeClass(g+" "+s+" "+i),o._isSliding=!1,setTimeout(function(){return t(o._element).trigger(I)},0)}).emulateTransitionEnd(600)):(t(a).removeClass(g),t(c).addClass(g),this._isSliding=!1,t(this._element).trigger(I)),C&&this.cycle()}},o._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s=r({},a,t(this).data());"object"==typeof e&&(s=r({},s,e));var l="string"==typeof e?e:s.slide;if(i||(i=new o(this,s),t(this).data(n,i)),"number"==typeof e)i.to(e);else if("string"==typeof l){if("undefined"==typeof i[l])throw new TypeError('No method named "'+l+'"');i[l]()}else s.interval&&(i.pause(),i.cycle())})},o._dataApiClickHandler=function(e){var i=P.getSelectorFromElement(this);if(i){var s=t(i)[0];if(s&&t(s).hasClass(_)){var a=r({},t(s).data(),t(this).data()),l=this.getAttribute("data-slide-to");l&&(a.interval=!1),o._jQueryInterface.call(t(s),a),l&&t(s).data(n).to(l),e.preventDefault()}}},s(o,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),o}();return t(document).on(d.CLICK_DATA_API,y.DATA_SLIDE,C._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(y.DATA_RIDE).each(function(){var e=t(this);C._jQueryInterface.call(e,e.data())})}),t.fn[e]=C._jQueryInterface,t.fn[e].Constructor=C,t.fn[e].noConflict=function(){return t.fn[e]=o,C._jQueryInterface},C}(e),H=function(t){var e="collapse",n="bs.collapse",i="."+n,o=t.fn[e],a={toggle:!0,parent:""},l={toggle:"boolean",parent:"(string|element)"},h={SHOW:"show"+i,SHOWN:"shown"+i,HIDE:"hide"+i,HIDDEN:"hidden"+i,CLICK_DATA_API:"click"+i+".data-api"},c="show",u="collapse",f="collapsing",d="collapsed",_="width",g="height",p={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},m=function(){function i(e,n){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(n),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var i=t(p.DATA_TOGGLE),s=0;s0&&(this._selector=o,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var o=i.prototype;return o.toggle=function(){t(this._element).hasClass(c)?this.hide():this.show()},o.show=function(){var e,s,r=this;if(!this._isTransitioning&&!t(this._element).hasClass(c)&&(this._parent&&0===(e=t.makeArray(t(this._parent).find(p.ACTIVES).filter('[data-parent="'+this._config.parent+'"]'))).length&&(e=null),!(e&&(s=t(e).not(this._selector).data(n))&&s._isTransitioning))){var o=t.Event(h.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){e&&(i._jQueryInterface.call(t(e).not(this._selector),"hide"),s||t(e).data(n,null));var a=this._getDimension();t(this._element).removeClass(u).addClass(f),this._element.style[a]=0,this._triggerArray.length>0&&t(this._triggerArray).removeClass(d).attr("aria-expanded",!0),this.setTransitioning(!0);var l=function(){t(r._element).removeClass(f).addClass(u).addClass(c),r._element.style[a]="",r.setTransitioning(!1),t(r._element).trigger(h.SHOWN)};if(P.supportsTransitionEnd()){var _="scroll"+(a[0].toUpperCase()+a.slice(1));t(this._element).one(P.TRANSITION_END,l).emulateTransitionEnd(600),this._element.style[a]=this._element[_]+"px"}else l()}}},o.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(c)){var n=t.Event(h.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",P.reflow(this._element),t(this._element).addClass(f).removeClass(u).removeClass(c),this._triggerArray.length>0)for(var s=0;s0&&t(n).toggleClass(d,!i).attr("aria-expanded",i)}},i._getTargetFromElement=function(e){var n=P.getSelectorFromElement(e);return n?t(n)[0]:null},i._jQueryInterface=function(e){return this.each(function(){var s=t(this),o=s.data(n),l=r({},a,s.data(),"object"==typeof e&&e);if(!o&&l.toggle&&/show|hide/.test(e)&&(l.toggle=!1),o||(o=new i(this,l),s.data(n,o)),"string"==typeof e){if("undefined"==typeof o[e])throw new TypeError('No method named "'+e+'"');o[e]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),i}();return t(document).on(h.CLICK_DATA_API,p.DATA_TOGGLE,function(e){"A"===e.currentTarget.tagName&&e.preventDefault();var i=t(this),s=P.getSelectorFromElement(this);t(s).each(function(){var e=t(this),s=e.data(n)?"toggle":i.data();m._jQueryInterface.call(e,s)})}),t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=o,m._jQueryInterface},m}(e),W=function(t){var e="dropdown",i="bs.dropdown",o="."+i,a=".data-api",l=t.fn[e],h=new RegExp("38|40|27"),c={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,CLICK:"click"+o,CLICK_DATA_API:"click"+o+a,KEYDOWN_DATA_API:"keydown"+o+a,KEYUP_DATA_API:"keyup"+o+a},u="disabled",f="show",d="dropup",_="dropright",g="dropleft",p="dropdown-menu-right",m="dropdown-menu-left",v="position-static",E='[data-toggle="dropdown"]',T=".dropdown form",y=".dropdown-menu",C=".navbar-nav",I=".dropdown-menu .dropdown-item:not(.disabled)",A="top-start",b="top-end",D="bottom-start",S="bottom-end",w="right-start",N="left-start",O={offset:0,flip:!0,boundary:"scrollParent"},k={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)"},L=function(){function a(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var l=a.prototype;return l.toggle=function(){if(!this._element.disabled&&!t(this._element).hasClass(u)){var e=a._getParentFromElement(this._element),i=t(this._menu).hasClass(f);if(a._clearMenus(),!i){var s={relatedTarget:this._element},r=t.Event(c.SHOW,s);if(t(e).trigger(r),!r.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof n)throw new TypeError("Bootstrap dropdown require Popper.js (https://popper.js.org)");var o=this._element;t(e).hasClass(d)&&(t(this._menu).hasClass(m)||t(this._menu).hasClass(p))&&(o=e),"scrollParent"!==this._config.boundary&&t(e).addClass(v),this._popper=new n(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===t(e).closest(C).length&&t("body").children().on("mouseover",null,t.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),t(this._menu).toggleClass(f),t(e).toggleClass(f).trigger(t.Event(c.SHOWN,s))}}}},l.dispose=function(){t.removeData(this._element,i),t(this._element).off(o),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},l.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},l._addEventListeners=function(){var e=this;t(this._element).on(c.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},l._getConfig=function(n){return n=r({},this.constructor.Default,t(this._element).data(),n),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},l._getMenuElement=function(){if(!this._menu){var e=a._getParentFromElement(this._element);this._menu=t(e).find(y)[0]}return this._menu},l._getPlacement=function(){var e=t(this._element).parent(),n=D;return e.hasClass(d)?(n=A,t(this._menu).hasClass(p)&&(n=b)):e.hasClass(_)?n=w:e.hasClass(g)?n=N:t(this._menu).hasClass(p)&&(n=S),n},l._detectNavbar=function(){return t(this._element).closest(".navbar").length>0},l._getPopperConfig=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets)||{}),e}:e.offset=this._config.offset,{placement:this._getPlacement(),modifiers:{offset:e,flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}}},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i);if(n||(n=new a(this,"object"==typeof e?e:null),t(this).data(i,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},a._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(E)),s=0;s0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},p._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},p._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},f="show",d="out",_={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,INSERTED:"inserted"+o,CLICK:"click"+o,FOCUSIN:"focusin"+o,FOCUSOUT:"focusout"+o,MOUSEENTER:"mouseenter"+o,MOUSELEAVE:"mouseleave"+o},g="fade",p="show",m=".tooltip-inner",v=".arrow",E="hover",T="focus",y="click",C="manual",I=function(){function a(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var I=a.prototype;return I.enable=function(){this._isEnabled=!0},I.disable=function(){this._isEnabled=!1},I.toggleEnabled=function(){this._isEnabled=!this._isEnabled},I.toggle=function(e){if(this._isEnabled)if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p))return void this._leave(null,this);this._enter(null,this)}},I.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},I.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var i=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(i);var s=t.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),o=P.getUID(this.constructor.NAME);r.setAttribute("id",o),this.element.setAttribute("aria-describedby",o),this.setContent(),this.config.animation&&t(r).addClass(g);var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(r).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(r).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:v},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(r).addClass(p),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d&&e._leave(null,e)};P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(this.tip).one(P.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},I.hide=function(e){var n=this,i=this.getTipElement(),s=t.Event(this.constructor.Event.HIDE),r=function(){n._hoverState!==f&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(s),s.isDefaultPrevented()||(t(i).removeClass(p),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[y]=!1,this._activeTrigger[T]=!1,this._activeTrigger[E]=!1,P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(i).one(P.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},I.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},I.isWithContent=function(){return Boolean(this.getTitle())},I.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},I.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},I.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(m),this.getTitle()),e.removeClass(g+" "+p)},I.setElementContent=function(e,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[i?"html":"text"](n)},I.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},I._getAttachment=function(t){return c[t.toUpperCase()]},I._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==C){var i=n===E?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,s=n===E?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(s,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},I._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},I._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T:E]=!0),t(n.getTipElement()).hasClass(p)||n._hoverState===f?n._hoverState=f:(clearTimeout(n._timeout),n._hoverState=f,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===f&&n.show()},n.config.delay.show):n.show())},I._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T:E]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d&&n.hide()},n.config.delay.hide):n.hide())},I._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},I._getConfig=function(n){return"number"==typeof(n=r({},this.constructor.Default,t(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},I._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},I._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},I._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},I._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(g),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i),s="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,s),t(this).data(i,n)),"string"==typeof e)){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return i}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return o}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=a,I._jQueryInterface},I}(e),x=function(t){var e="popover",n="bs.popover",i="."+n,o=t.fn[e],a=new RegExp("(^|\\s)bs-popover\\S+","g"),l=r({},U.Default,{placement:"right",trigger:"click",content:"",template:''}),h=r({},U.DefaultType,{content:"(string|element|function)"}),c="fade",u="show",f=".popover-header",d=".popover-body",_={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},g=function(r){var o,g;function p(){return r.apply(this,arguments)||this}g=r,(o=p).prototype=Object.create(g.prototype),o.prototype.constructor=o,o.__proto__=g;var m=p.prototype;return m.isWithContent=function(){return this.getTitle()||this._getContent()},m.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},m.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},m.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(f),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(d),n),e.removeClass(c+" "+u)},m._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},m._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(a);null!==n&&n.length>0&&e.removeClass(n.join(""))},p._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s="object"==typeof e?e:null;if((i||!/destroy|hide/.test(e))&&(i||(i=new p(this,s),t(this).data(n,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},s(p,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return l}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return n}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return h}}]),p}(U);return t.fn[e]=g._jQueryInterface,t.fn[e].Constructor=g,t.fn[e].noConflict=function(){return t.fn[e]=o,g._jQueryInterface},g}(e),K=function(t){var e="scrollspy",n="bs.scrollspy",i="."+n,o=t.fn[e],a={offset:10,method:"auto",target:""},l={offset:"number",method:"string",target:"(string|element)"},h={ACTIVATE:"activate"+i,SCROLL:"scroll"+i,LOAD_DATA_API:"load"+i+".data-api"},c="dropdown-item",u="active",f={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},d="offset",_="position",g=function(){function o(e,n){var i=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(n),this._selector=this._config.target+" "+f.NAV_LINKS+","+this._config.target+" "+f.LIST_ITEMS+","+this._config.target+" "+f.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,t(this._scrollElement).on(h.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var g=o.prototype;return g.refresh=function(){var e=this,n=this._scrollElement===this._scrollElement.window?d:_,i="auto"===this._config.method?n:this._config.method,s=i===_?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.makeArray(t(this._selector)).map(function(e){var n,r=P.getSelectorFromElement(e);if(r&&(n=t(r)[0]),n){var o=n.getBoundingClientRect();if(o.width||o.height)return[t(n)[i]().top+s,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},g.dispose=function(){t.removeData(this._element,n),t(this._scrollElement).off(i),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},g._getConfig=function(n){if("string"!=typeof(n=r({},a,n)).target){var i=t(n.target).attr("id");i||(i=P.getUID(e),t(n.target).attr("id",i)),n.target="#"+i}return P.typeCheckConfig(e,n,l),n},g._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},g._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},g._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},g._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;){this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=P,t.Alert=L,t.Button=R,t.Carousel=j,t.Collapse=H,t.Dropdown=W,t.Modal=M,t.Popover=x,t.Scrollspy=K,t.Tab=V,t.Tooltip=U,Object.defineProperty(t,"__esModule",{value:!0})}); 7 | //# sourceMappingURL=bootstrap.min.js.map --------------------------------------------------------------------------------