├── .dockerignore ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── nodejs.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config └── default.json ├── docker-compose.debug.yml ├── docker-compose.mon.yml ├── docker-compose.yml ├── nodemon.json ├── package-lock.json ├── package.json ├── plugins ├── admin.js ├── auth.js ├── code.js ├── core.js ├── echo.js ├── evaluate.js ├── factoid.js ├── google.js ├── imaging.js ├── livescore.js ├── natural.js ├── poll.js ├── pose.js ├── record.js ├── reminders.js ├── remove_bg.js ├── sticker.js ├── tell.js ├── translate.js └── trivia.js ├── src ├── app.js └── super-bot │ ├── Message.js │ ├── MessageBuilder.js │ ├── Middleware.js │ ├── SuperBot.js │ ├── SuperBotProxy.js │ └── index.js ├── start.js └── test ├── plugins └── plugin.echo.test.js └── setup.test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/charts 16 | **/docker-compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | README.md 24 | **/plugins 25 | **/config -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_APPLICATION_CREDENTIALS="PATH" 2 | GOOGLE_PROJECT_ID="project-id" 3 | CAPTCHA_SOLVER_TOKEN="token" 4 | SUPERBOT_ADMIN_SECRET="secret" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | clients/whatsapp/user_data/ 63 | data/ 64 | Superbot-807b1a9f4f85.json 65 | clients/whatsapp/screenshot.png 66 | rb_user_data/ 67 | temp/ 68 | Superbot-c457375bf8a5.json 69 | .vscode/ 70 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1-alpine3.11 2 | 3 | ENV NODE_ENV production 4 | 5 | RUN apk add --no-cache \ 6 | build-base \ 7 | g++ \ 8 | cairo-dev \ 9 | jpeg-dev \ 10 | pango-dev \ 11 | bash \ 12 | imagemagick 13 | 14 | RUN apk --no-cache add msttcorefonts-installer fontconfig && \ 15 | update-ms-fonts && \ 16 | fc-cache -f 17 | 18 | WORKDIR /usr/src/app 19 | 20 | COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] 21 | 22 | RUN npm install --production --silent && mv node_modules ../ 23 | 24 | COPY . . 25 | 26 | EXPOSE 3000 27 | 28 | CMD npm start -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 George Faraj 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/gfaraj/super-bot/workflows/Node%20CI/badge.svg)](https://github.com/gfaraj/super-bot/actions) 2 | 3 | 4 | # super-bot 5 | A simple but extensible bot written in Node.js. Currently there are Whatsapp, Slack, and Discord interfaces, but any messaging platform can be supported by creating a client for it (e.g FB Messenger, MS Teams, IRC). 6 | 7 | ## Docker 8 | 9 | Running the bot service as a docker container is not yet supported due to incompatibility issues with the database provider. The bot clients are all dockerized, though, and [here is a handy docker-compose file](https://gist.github.com/gfaraj/e9459a5d90cdcb923655f70ae456b96a) to quickly start all of them. 10 | 11 | ## Installing from source 12 | 13 | Clone this repository: 14 | 15 | ``` 16 | git clone https://github.com/gfaraj/super-bot.git 17 | ``` 18 | 19 | and install its dependencies by running: 20 | 21 | ``` 22 | npm install 23 | ``` 24 | 25 | Make sure you have npm and Node 10 or newer installed. 26 | 27 | ## Starting the bot 28 | 29 | You can run the bot service with the following command: 30 | 31 | ``` 32 | npm run start 33 | ``` 34 | 35 | The bot service starts a web server on port 3000 (which is configurable) and accepts POST requests on "/message" with the body being JSON data representing a message object. The bot then will run that message through any commands that are supported and sends back the response. This makes it really easy to have any number of interfaces tied to the same bot service. Currently there's no security to restrict access to this endpoint so be aware that anyone could post messages to the bot. 36 | 37 | ## Configuration 38 | 39 | The bot uses a JSON configuration file located in the ./config folder. See the [config](https://docs.npmjs.com/cli/config) package documentation for more information. The Whatsapp client is also configured this way. 40 | 41 | ## Clients 42 | 43 | The same bot service can be utilized through different clients. The clients are how users interface with the bot service. They handle all the platform-specific tasks like listening for new messages and transforming them into a standard message object for the bot service. 44 | 45 | | Repo | Description 46 | |--- |--- 47 | | [super-bot-whatsapp](https://github.com/gfaraj/super-bot-whatsapp) | Chat interface for Whatsapp 48 | | [super-bot-slack](https://github.com/gfaraj/super-bot-slack) | Chat interface for Slack 49 | | [super-bot-discord](https://github.com/gfaraj/super-bot-discord) | Chat interface for Discord 50 | 51 | ## Plugins 52 | 53 | The bot is driven by plugins. Each plugin can define any number of commands that it supports (a command can only be supported by a single plugin). A plugin can also subscribe to be called whenever a message has not been handled by any command (for example, as a fallback). It's required that a plugin export a default function that will be called during initialization. 54 | 55 | ``` 56 | export default function(bot) { 57 | bot.command('echo', (bot, message) => { 58 | bot.respond({ text : message.text, attachment : message.attachment }); // just respond back with the same message. 59 | }); 60 | bot.raw((bot, message, next) => { 61 | if (message.text.includes('foo')) { // check if we can handle this raw message. 62 | bot.respond('bar'); 63 | } 64 | else { 65 | next(); // call next if your plugin can't handle this message. 66 | } 67 | }); 68 | } 69 | ``` 70 | 71 | ### record 72 | 73 | This plugin allows you (and your friends) to record key-value pairs with optional attachments (currently only images and Whatsapp stickers are supported). The recordings are scoped per chat. There is a plan to support global recordings in the future. 74 | 75 | ``` 76 | record 77 | ``` 78 | 79 | The value is optional if an attachment is provided. 80 | 81 | The plugin also registers the "forget" command to remove a recording. Only you or the recording's author are allowed to do this. It also registers the "recordings" command which sends a list of all existing recordings that match a given string (or all if no string is provided). 82 | 83 | Examples: 84 | ``` 85 | record hi Hello everyone! 86 | ``` 87 | After doing that, when the bot is given the command "hi" it will respond with "Hello everyone!". 88 | ``` 89 | (as a reply to an image message) 90 | record kids 91 | ``` 92 | The Whatsapp client will append any replied message to the current command, so in the example above it will save a "kids" recording with the image attachment. Then when the bot is given the "kids" command, it will send that image. 93 | 94 | ``` 95 | record stocks !google stock msft 96 | ``` 97 | This is an unofficial way to write shortcuts to other commands. After the command above, if given the "stocks" command, the bot will respond with "!google stock msft" and it will then respond to that command. In the future, a proper command chaining feature based on piping is planned. 98 | 99 | ### translate 100 | 101 | This plugin translates a given text into a target language like so: 102 | 103 | ``` 104 | translate es Hi, how are you doing? 105 | ``` 106 | It takes a two-letter locale as the first parameter and the text to translate after it. 107 | 108 | This uses the Google Translate API to perform the translation. You need to [set up a Google Cloud project](https://cloud.google.com/translate/docs/quickstart-client-libraries#client-libraries-usage-nodejs) with Translate support to be able to use this plugin. There is a cost to this if the free quotas are exceeded, so be mindful of that. 109 | 110 | ### google 111 | 112 | This plugin will perform a Google search and return the first 2 results. It's currently scraping the google search results (using puppeteer to process and render the initial web page, then using cheerio to grab the information). This is not permitted by Google so use at your own risk. 113 | ``` 114 | google game of thrones 115 | ``` 116 | The response is something like: 117 | ``` 118 | 1) https://www.hbo.com/game-of-thrones 119 | 2) https://en.wikipedia.org/wiki/Game_of_Thrones 120 | ``` 121 | 122 | This plugin also exposes a "gimg" command that performs a Google Image Search and sends back the first image result (sends the actual image, not just a link). Currently it's returning a low-res image but it's possible to adjust it to get the original image. 123 | 124 | ### evaluate 125 | 126 | This plugin will evaluate an expression and respond with the result. It exposes two commands, eval and calc. The eval command accepts any type of (limited - it uses safe-eval) javascript expression, while the calc command will only work with numeric expressions. 127 | 128 | ``` 129 | eval 'Hello' + ' world!' 130 | calc 10 + 20 / 2 131 | calc 15 - sqrt(4) 132 | ``` 133 | Responds with: 134 | ``` 135 | Hello world! 136 | 20 137 | 13 138 | ``` 139 | 140 | ### sticker 141 | 142 | This plugin provides utilities related to Whatsapp stickers. The "stickerize" command will take any image and convert it to a Whatsapp sticker. 143 | 144 | ``` 145 | (as a reply to an image message) 146 | stickerize 147 | ``` 148 | This would send a message with the quoted image as a sticker. 149 | 150 | ### code 151 | 152 | This plugin allows you to build and run code in many languages. It will respond with any build errors or any output generated by your program. You can use this plugin like this: 153 | 154 | `!code -l ` 155 | 156 | Examples: 157 | 158 | `!code -lc# public static class P { public static void Main() { System.Console.WriteLine("Hello, world!\n"); } }` 159 | 160 | `!code -lgo package main 161 | import "fmt" 162 | func main() { 163 | fmt.Printf(''Have fun!") 164 | }` 165 | 166 | There are a couple of shortcuts for C# and C++ currently and more can be added easily: 167 | 168 | `!cs public static class P { public static void Main() { System.Console.WriteLine("Hello, world!\n"); } }` 169 | 170 | ``` 171 | !c++ #include 172 | int main() { std::cout << "Hey there!\n"; } 173 | ``` 174 | 175 | The plugin also supports passing in a code file as an attachment. 176 | 177 | ### Other commands 178 | 179 | There are many other commands available like remindme, trivia, poll, factoid, and several imaging utilities. 180 | 181 | ## Contribution 182 | 183 | Contribution of any kind is welcome! Please feel free to create your own plugins for the bot service, chat interfaces, features or bug fixes. Fork the repository, create your own branch and submit pull requests. 184 | 185 | # Disclaimer 186 | 187 | This project was done for educational purposes. This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by WhatsApp or any of its affiliates or subsidiaries. This is an independent and unofficial software. Use at your own risk. 188 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "SuperBot" : { 3 | "pluginsPath": "./plugins", 4 | "enablePipe": true, 5 | "pipeDelimeter": "|", 6 | "plugins": [ 7 | { "name": "auth" }, 8 | { "name": "admin" }, 9 | { "name": "core" }, 10 | { "name": "pose" }, 11 | { "name": "echo" }, 12 | { "name": "evaluate" }, 13 | { "name": "factoid" }, 14 | { "name": "google" }, 15 | { "name": "imaging" }, 16 | { "name": "livescore" }, 17 | { "name": "natural" }, 18 | { "name": "poll" }, 19 | { "name": "record" }, 20 | { "name": "reminders" }, 21 | { "name": "remove_bg" }, 22 | { "name": "sticker" }, 23 | { "name": "tell" }, 24 | { "name": "translate" }, 25 | { "name": "trivia" }, 26 | { "name": "code" } 27 | ] 28 | }, 29 | 30 | "app": { 31 | "port": 3000 32 | }, 33 | 34 | "greeting": "Hi! I'm a bot that responds to your commands." 35 | } -------------------------------------------------------------------------------- /docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | super-bot: 5 | image: gfaraj/super-bot:latest 6 | build: . 7 | env_file: 8 | - .env 9 | environment: 10 | NODE_ENV: development 11 | volumes: 12 | - type: bind 13 | source: . 14 | target: /usr/src/app 15 | ports: 16 | - "3000:3000" 17 | - "9229:9229" 18 | command: node --inspect=0.0.0.0:9229 . -------------------------------------------------------------------------------- /docker-compose.mon.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | super-bot: 5 | image: gfaraj/super-bot:latest 6 | build: . 7 | env_file: 8 | - .env 9 | environment: 10 | NODE_ENV: production 11 | volumes: 12 | - type: bind 13 | source: . 14 | target: /usr/src/app 15 | ports: 16 | - "3000:3000" 17 | command: npm run start:mon -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | super-bot: 5 | image: gfaraj/super-bot:latest 6 | build: . 7 | env_file: 8 | - .env 9 | environment: 10 | NODE_ENV: production 11 | volumes: 12 | - type: bind 13 | source: . 14 | target: /usr/src/app 15 | ports: 16 | - "3000:3000" -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": [ 4 | "data/*", 5 | "temp/*", 6 | "node_modules/**/node_modules" 7 | ], 8 | "execMap": { 9 | "js": "node --harmony" 10 | }, 11 | "watch": [ 12 | "plugins/", 13 | "src/", 14 | "config/" 15 | ] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super-bot", 3 | "version": "2.0.0", 4 | "description": "A simple yet extensible bot.", 5 | "main": "start.js", 6 | "dependencies": { 7 | "@google-cloud/translate": "^4.1.3", 8 | "axios": "^0.19.0", 9 | "canvas": "^2.6.0", 10 | "cheerio": "^1.0.0-rc.3", 11 | "config": "^3.2.2", 12 | "dialogflow": "^0.12.2", 13 | "dotenv": "^8.1.0", 14 | "esm": "^3.2.25", 15 | "express": "^4.17.1", 16 | "lodash": "^4.17.15", 17 | "moment": "^2.24.0", 18 | "nedb": "^1.8.0", 19 | "puppeteer": "^1.20.0", 20 | "puppeteer-extra": "^2.1.3", 21 | "puppeteer-extra-plugin-recaptcha": "^3.0.4", 22 | "puppeteer-extra-plugin-stealth": "^2.2.2", 23 | "safe-eval": "^0.4.1", 24 | "sharp": "^0.23.1" 25 | }, 26 | "devDependencies": { 27 | "mocha": "^6.2.2", 28 | "nodemon": "^2.0.2", 29 | "should": "^13.2.3" 30 | }, 31 | "scripts": { 32 | "start": "node .", 33 | "start:dev": "node .", 34 | "start:mon": "nodemon -L .", 35 | "test": "mocha --recursive --require esm --exit" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/gfaraj/super-bot.git" 40 | }, 41 | "keywords": [ 42 | "bot", 43 | "robot", 44 | "respond" 45 | ], 46 | "author": "George Faraj", 47 | "license": "ISC", 48 | "bugs": { 49 | "url": "https://github.com/gfaraj/super-bot/issues" 50 | }, 51 | "homepage": "https://github.com/gfaraj/super-bot#readme" 52 | } 53 | -------------------------------------------------------------------------------- /plugins/admin.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , ignoresDb = new Datastore({ filename: './data/ignores.db', autoload: true }); 3 | 4 | ignoresDb.ensureIndex({ fieldName: 'senderId' }, function (err) { 5 | }); 6 | 7 | ignoresDb.ensureIndex({ fieldName: 'chatId' }, function (err) { 8 | }); 9 | 10 | export default function(bot) { 11 | bot.use((bot, message, next) => { 12 | if (message.sender.isAdmin) { 13 | next(); 14 | } 15 | else { 16 | ignoresDb.findOne({ $or: [{ senderId: message.sender.id }, { chatId: message.chat.id }] }, function (err, doc) { 17 | if (!doc) { 18 | next(); 19 | } 20 | }); 21 | } 22 | }); 23 | 24 | bot.command('ignore', (bot, message) => { 25 | if (!message.sender.isAdmin) { 26 | bot.error('You don\'t have permission to ignore.'); 27 | return; 28 | } 29 | if (message.text.length == 0) { 30 | bot.error('Please specify the user id to ignore.'); 31 | return; 32 | } 33 | ignoresDb.update({ senderId : message.text }, { senderId : message.text }, { upsert : true }, function () { 34 | bot.respond(`I'm now ignoring "${message.text}".`); 35 | }); 36 | }); 37 | 38 | bot.command('unignore', (bot, message) => { 39 | if (!message.sender.isAdmin) { 40 | bot.error('You don\'t have permission to unignore.'); 41 | return; 42 | } 43 | if (message.text.length == 0) { 44 | bot.error('Please specify the user id to unignore.'); 45 | return; 46 | } 47 | ignoresDb.remove({ senderId : message.text }, { }, function (err, numRemoved) { 48 | if (numRemoved > 0) { 49 | bot.respond(`I'm now listening to "${message.text}".`); 50 | } 51 | else { 52 | bot.error(`I was not ignoring "${message.text}".`); 53 | } 54 | }); 55 | }); 56 | 57 | bot.command('silence', (bot, message) => { 58 | if (!message.sender.isAdmin) { 59 | bot.error('You don\'t have permission to silence.'); 60 | return; 61 | } 62 | ignoresDb.update({ chatId : message.chat.id }, { chatId : message.chat.id }, { upsert : true }, function () { 63 | bot.respond(`I'll be silent here now.`); 64 | }); 65 | }); 66 | 67 | bot.command('unsilence', (bot, message) => { 68 | if (!message.sender.isAdmin) { 69 | bot.error('You don\'t have permission to silence.'); 70 | return; 71 | } 72 | ignoresDb.remove({ chatId : message.chat.id }, { }, function (err, numRemoved) { 73 | if (numRemoved > 0) { 74 | bot.respond(`I'm now listening here.`); 75 | } 76 | else { 77 | bot.error(`I was not silent here.`); 78 | } 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /plugins/auth.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , adminsDb = new Datastore({ filename: './data/admins.db', autoload: true }); 3 | 4 | adminsDb.ensureIndex({ fieldName: 'senderId' }, function (err) { 5 | }); 6 | 7 | export function isAdmin(senderId) { 8 | return new Promise((resolve, reject) => { 9 | adminsDb.findOne({ senderId: senderId }, function (err, doc) { 10 | resolve(!!doc); 11 | }); 12 | }); 13 | } 14 | 15 | export function isOwner(senderId) { 16 | return new Promise((resolve, reject) => { 17 | adminsDb.findOne({ senderId: senderId, isOwner: true }, function (err, doc) { 18 | resolve(!!doc); 19 | }); 20 | }); 21 | } 22 | 23 | export default function(bot) { 24 | bot.use(async (bot, message, next) => { 25 | if (message.sender.isMe || await isAdmin(message.sender.id)) { 26 | message.sender.isAdmin = true; 27 | } 28 | 29 | next(); 30 | }); 31 | 32 | bot.command('auth', (bot, message) => { 33 | if (message.text.length == 0) { 34 | bot.error('Please specify the admin secret.'); 35 | return; 36 | } 37 | let isAdmin = message.text.trim() === process.env.SUPERBOT_ADMIN_SECRET; 38 | let isOwner = message.text.trim() === process.env.SUPERBOT_OWNER_SECRET; 39 | 40 | if (isAdmin || isOwner) { 41 | adminsDb.update({ senderId: message.sender.id }, { senderId: message.sender.id, isOwner }, { upsert: true }, function () { 42 | bot.respond(`You have been successfully authenticated as ${isOwner ? 'owner' : 'admin'}.`); 43 | }); 44 | } 45 | else { 46 | bot.error('Incorrect admin secret specified.'); 47 | } 48 | }); 49 | 50 | bot.command('dethrone', async (bot, message) => { 51 | if (!await isOwner(message.sender.id)) { 52 | bot.error('You don\'t have permission to de-throne.'); 53 | return; 54 | } 55 | if (message.text.length == 0) { 56 | bot.error('Please specify the user id to de-throne.'); 57 | return; 58 | } 59 | 60 | adminsDb.remove({ senderId: message.text }, { }, function (err, numRemoved) { 61 | if (numRemoved > 0) { 62 | bot.respond(`I've de-throned ${message.text}.`); 63 | } 64 | else { 65 | bot.error(`I was not able to de-throne this user, perhaps he's not an admin.`); 66 | } 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /plugins/code.js: -------------------------------------------------------------------------------- 1 | const crawl_url = 'https://ideone.com'; 2 | const puppeteer = require('puppeteer-extra'); 3 | const path = require('path'); 4 | 5 | const pluginStealth = require('puppeteer-extra-plugin-stealth'); 6 | puppeteer.use(pluginStealth()); 7 | 8 | const pluginRecaptcha = require('puppeteer-extra-plugin-recaptcha'); 9 | puppeteer.use(pluginRecaptcha({ 10 | provider: {id: '2captcha', token: process.env.CAPTCHA_SOLVER_TOKEN}, 11 | visualFeedback: true 12 | })); 13 | 14 | var browser; 15 | 16 | const DEFAULT_CHROMIUM_ARGS = [ 17 | //"--disable-gpu", 18 | "--renderer", 19 | "--no-sandbox", 20 | "--no-service-autorun", 21 | "--no-experiments", 22 | "--no-default-browser-check", 23 | //"--disable-webgl", 24 | "--disable-threaded-animation", 25 | "--disable-threaded-scrolling", 26 | "--disable-in-process-stack-traces", 27 | "--disable-histogram-customizer", 28 | //"--disable-gl-extensions", 29 | "--disable-extensions", 30 | "--disable-composited-antialiasing", 31 | //"--disable-canvas-aa", 32 | "--disable-3d-apis", 33 | //"--disable-accelerated-2d-canvas", 34 | //"--disable-accelerated-jpeg-decoding", 35 | "--disable-accelerated-mjpeg-decode", 36 | "--disable-app-list-dismiss-on-blur", 37 | "--disable-accelerated-video-decode", 38 | //"--num-raster-threads=1", 39 | ]; 40 | 41 | async function runCode(language, codeText) { 42 | if (!browser) { 43 | browser = await puppeteer.launch({ 44 | headless: true, 45 | userDataDir: path.resolve("./temp/code_user_data"), 46 | args: DEFAULT_CHROMIUM_ARGS, 47 | ignoreHTTPSErrors: true, 48 | devtools: false, 49 | defaultViewport: null 50 | }); 51 | } 52 | 53 | await browser.pages(); 54 | 55 | let page = await browser.newPage(); 56 | await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'); 57 | await page.setBypassCSP(true); 58 | await page.goto(crawl_url, { 59 | waitUntil: 'networkidle0', 60 | timeout: 0 61 | }); 62 | 63 | await page.waitFor('#Submit', {timeout: 8000}); 64 | 65 | try { 66 | const languageId = await page.$eval(`#lang-dropdown-menu a[data-label^="${language}" i]`, a => a.getAttribute('data-id')); 67 | 68 | if (!languageId) { 69 | throw "Unsupported language specified!"; 70 | } 71 | 72 | const setInputValue = async (selector, value) => { 73 | await page.waitFor(selector); 74 | await page.evaluate((data) => { 75 | return document.querySelector(data.selector).value = data.value; 76 | }, {selector, value}); 77 | }; 78 | 79 | let isSyntaxVisible = await page.$eval('label[for="syntax"]', elem => { 80 | return window.getComputedStyle(elem).getPropertyValue('display') !== 'none' && !!elem.offsetHeight; 81 | }); 82 | 83 | if (!isSyntaxVisible) { 84 | await page.click('#button-more-options'); 85 | await page.waitFor(1500); 86 | } 87 | 88 | const isSyntaxChecked = await page.$eval('#syntax', elem => { 89 | return elem.checked; 90 | }); 91 | 92 | isSyntaxVisible = await page.$eval('label[for="syntax"]', elem => { 93 | return window.getComputedStyle(elem).getPropertyValue('display') !== 'none' && elem.offsetHeight; 94 | }); 95 | 96 | if (isSyntaxChecked) { 97 | await page.click('label[for="syntax"]'); 98 | await page.waitFor(500); 99 | } 100 | 101 | await setInputValue('#_lang', languageId); 102 | await setInputValue('#file', codeText); 103 | 104 | await page.click('#Submit'); 105 | 106 | const statusElement = await page.waitForSelector('#view_status .info.green, #view_status .info.red'); 107 | 108 | if (await page.evaluate(e => e.classList.contains("red"), statusElement)) { 109 | const errorText = await page.$eval('#view_cmperr_content', e => e.textContent); 110 | return errorText || 'Unknown build error encountered.'; 111 | } 112 | 113 | await page.waitFor('#output-text', {timeout: 8000}); 114 | 115 | const output = await page.$eval('#output-text', e => e.textContent); 116 | return output; 117 | } 118 | finally { 119 | await page.close(); 120 | } 121 | } 122 | 123 | function autoDetectLanguageFromAttachment(attachment) { 124 | if (attachment.filetype) { 125 | switch (attachment.filetype) { 126 | case "csharp": return "c#"; 127 | case "cpp": return "c++"; 128 | } 129 | } 130 | return null; 131 | } 132 | 133 | async function handleCode(bot, message, language) { 134 | let text = message.text.trim(); 135 | 136 | if (!language && text.startsWith('-l')) { 137 | const split = text.replace(/\s+/, '\x01').split('\x01'); 138 | language = split[0].substring(2); 139 | if (split.length > 1) { 140 | text = split[1].trim(); 141 | } 142 | } 143 | 144 | if (!text && message.attachment) { 145 | if (message.attachment) { 146 | let dataSplit = message.attachment.data.split(','); 147 | let mimetype = dataSplit[0].match(/:(.*?);/)[1]; 148 | let data = dataSplit[1]; 149 | 150 | if (!mimetype.startsWith('text')) { 151 | bot.error('The attachment is not valid code!'); 152 | return; 153 | } 154 | 155 | text = atob(data); 156 | 157 | if (!language) { 158 | language = autoDetectLanguageFromAttachment(message.attachment); 159 | } 160 | } 161 | } 162 | 163 | if (!text) { 164 | bot.error('Please specify the code to run!'); 165 | return; 166 | } 167 | 168 | if (!language) { 169 | bot.error('Please specify a programming language!'); 170 | return; 171 | } 172 | 173 | try { 174 | let result = await runCode(language, text); 175 | if (result) { 176 | bot.respond(bot.new().text(result)); 177 | } 178 | else { 179 | bot.error('I could not run this code.'); 180 | } 181 | } 182 | catch (error) { 183 | console.log(error.stack); 184 | bot.error('I could not run this code at this time.'); 185 | } 186 | } 187 | 188 | export default function(bot) { 189 | bot.command('code', handleCode); 190 | bot.command('cs', (bot, message) => handleCode(bot, message, 'c#')); 191 | bot.command('c++', (bot, message) => handleCode(bot, message, 'c++')); 192 | } 193 | -------------------------------------------------------------------------------- /plugins/core.js: -------------------------------------------------------------------------------- 1 | export default function(bot) { 2 | bot.command('bot', (bot, message) => { 3 | bot.respond({ text : require('config').get('greeting') || 'Hello there human!' }); 4 | }); 5 | bot.command('cleartext', async (bot, message) => { 6 | bot.respond(bot.copy(message) 7 | .text('')); 8 | }); 9 | bot.command('clearattach', async (bot, message) => { 10 | bot.respond(bot.copy(message) 11 | .attachment({})); 12 | }); 13 | bot.command('clear', async (bot, message) => { 14 | bot.pass(bot.copy(message) 15 | .text('cleartext') 16 | .pipe('clearattach')); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /plugins/echo.js: -------------------------------------------------------------------------------- 1 | export default function(bot) { 2 | bot.command('echo', (bot, message) => { 3 | bot.respond(bot.new().text(message.text).attachment(message.attachment)); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /plugins/evaluate.js: -------------------------------------------------------------------------------- 1 | 2 | const safeEval = require('safe-eval'); 3 | 4 | function evaluate(bot, message) { 5 | if (message.text == null || message.text.length == 0) { 6 | bot.error('Give me an expression to evaluate!'); 7 | return; 8 | } 9 | try { 10 | bot.respond(`${safeEval(message.text)}`); 11 | } 12 | catch (err) { 13 | console.log(err); 14 | bot.error('Your expression could not be evaluated.'); 15 | } 16 | } 17 | 18 | function calculate(bot, message) { 19 | if (message.text == null || message.text.length == 0) { 20 | bot.error('Give me an expression to calculate!'); 21 | return; 22 | } 23 | try { 24 | let expression = message.text; 25 | Object.getOwnPropertyNames(Math).forEach(key => { 26 | expression = expression.replace(key, `Math.${key}`); 27 | }); 28 | 29 | bot.respond(`${safeEval('1 * (' + expression + ')')}`); 30 | } 31 | catch (err) { 32 | console.log(err); 33 | bot.error('Please specify a valid mathematical expression.'); 34 | } 35 | } 36 | 37 | export default function(bot) { 38 | bot.command('eval', evaluate); 39 | bot.command('calc', calculate); 40 | } 41 | -------------------------------------------------------------------------------- /plugins/factoid.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , db = new Datastore({ filename: './data/factoid.db', autoload: true }); 3 | 4 | db.ensureIndex({ fieldName: 'chatId' }, function (err) { 5 | }); 6 | 7 | function handleFactoid(bot, message) { 8 | if (message.text.length == 0) { 9 | db.count({ chatId : message.chat.id }, function (err, count) { 10 | if (count === 0) { 11 | bot.error('There are no factoids here.'); 12 | return; 13 | } 14 | 15 | let index = Math.floor(Math.random() * Math.floor(count)); 16 | 17 | db.find({ chatId : message.chat.id }) 18 | .skip(index) 19 | .limit(1) 20 | .exec(function (err, docs) { 21 | if (err || docs.length === 0) { 22 | bot.error('I could not retrieve a factoid.'); 23 | return; 24 | } 25 | let doc = docs[0]; 26 | bot.respond({ text : doc.text, attachment : doc.attachment }); 27 | }); 28 | }); 29 | } 30 | else { 31 | let doc = { 32 | chatId : message.chat.id, 33 | authorId: message.sender.id, 34 | text : message.text, 35 | attachment : message.attachment 36 | }; 37 | db.insert(doc, function () { 38 | bot.respond(`I've recorded your factoid.`); 39 | }); 40 | } 41 | } 42 | 43 | 44 | export default function(bot) { 45 | bot.command('factoid', handleFactoid); 46 | } 47 | -------------------------------------------------------------------------------- /plugins/google.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const querystring = require('querystring'); 3 | const cheerio = require('cheerio'); 4 | const google_search_url = 'http://www.google.com/search?'; 5 | const max_results = 2; 6 | const puppeteer = require('puppeteer'); 7 | 8 | var browser; 9 | 10 | function extractGoogleResultsFinal(body) { 11 | return new Promise(function (resolve, reject) { 12 | var results = []; 13 | var $ = cheerio.load(body); 14 | 15 | $('#main > div > div > div > a').each(function(i, elem) { 16 | var item = {}; 17 | 18 | var elemUrl = $(this); 19 | var elemDesc = $(this).find('div'); 20 | var url = elemUrl.attr("href"); 21 | var parsedUrl = require('url').parse(url, true); 22 | if (parsedUrl.pathname === '/url') { 23 | item['url'] = parsedUrl.query.q; 24 | } 25 | else { 26 | item['url'] = url; 27 | } 28 | item['title'] = elemDesc.text(); 29 | 30 | results.push(item); 31 | 32 | if (results.length >= max_results) 33 | return false; 34 | }); 35 | 36 | resolve(results); 37 | }); 38 | } 39 | 40 | async function getImageData(url) { 41 | var res = await axios.get(url, { responseType: 'arraybuffer' }); 42 | 43 | return `data:${res.headers['content-type']};base64,${Buffer.from(String.fromCharCode(...new Uint8Array(res.data)), 'binary') 44 | .toString('base64')}`; 45 | } 46 | 47 | async function extractGoogleImageResultFinal(body) { 48 | var results; 49 | var $ = cheerio.load(body); 50 | 51 | $('#search > div > div > div > div > div > div > div > a > img').each(function(i, elem) { 52 | //$('.images_table img').each(function(i, elem) { 53 | if (!results) { 54 | results = $(this).attr('src'); 55 | } 56 | }); 57 | 58 | if (!results) { 59 | $('div[rfpps] img[data-atf="true"]').each(function(i, elem) { 60 | if (!results) { 61 | results = $(this).attr('src'); 62 | } 63 | }); 64 | } 65 | 66 | if (!results) { 67 | $('div[data-cid] img[data-atf="true"]').each(function(i, elem) { 68 | if (!results) { 69 | results = $(this).attr('src'); 70 | } 71 | }); 72 | } 73 | 74 | return Promise.resolve(results); 75 | 76 | /*if (results) 77 | return await getImageData(results); 78 | return null;*/ 79 | } 80 | 81 | const DEFAULT_CHROMIUM_ARGS = [ 82 | //"--disable-gpu", 83 | "--renderer", 84 | "--no-sandbox", 85 | "--no-service-autorun", 86 | "--no-experiments", 87 | "--no-default-browser-check", 88 | //"--disable-webgl", 89 | "--disable-threaded-animation", 90 | "--disable-threaded-scrolling", 91 | "--disable-in-process-stack-traces", 92 | "--disable-histogram-customizer", 93 | //"--disable-gl-extensions", 94 | "--disable-extensions", 95 | "--disable-composited-antialiasing", 96 | //"--disable-canvas-aa", 97 | "--disable-3d-apis", 98 | //"--disable-accelerated-2d-canvas", 99 | //"--disable-accelerated-jpeg-decoding", 100 | "--disable-accelerated-mjpeg-decode", 101 | "--disable-app-list-dismiss-on-blur", 102 | "--disable-accelerated-video-decode", 103 | //"--num-raster-threads=1", 104 | ]; 105 | 106 | async function extractGoogleResults(body, callback) { 107 | if (!browser) { 108 | browser = await puppeteer.launch({ 109 | headless: true, 110 | args: DEFAULT_CHROMIUM_ARGS, 111 | ignoreHTTPSErrors: true, 112 | devtools: false, 113 | defaultViewport: null 114 | }); 115 | } 116 | 117 | await browser.pages(); 118 | 119 | let page = await browser.newPage(); 120 | await page.setContent(body, { waitUntil: 'networkidle0'}); 121 | 122 | return await callback(await page.content()); 123 | } 124 | 125 | function handleGoogle(bot, message) { 126 | let params = { 127 | hl : 'en', 128 | q : message.text, 129 | start : 0, 130 | num : max_results 131 | }; 132 | axios.get(google_search_url + querystring.stringify(params)) 133 | .then(async response => { 134 | let results = await extractGoogleResults(response.data, extractGoogleResultsFinal); 135 | let resultText = ''; 136 | results.forEach((r, i) => { 137 | resultText += `${i+1}) ${r.url}\n`; 138 | }); 139 | bot.respond(resultText.length == 0 ? 'I didn\'t find anything' : resultText); 140 | }) 141 | .catch(error => { 142 | bot.error(`Sorry, I\'m having trouble contacting Google right now. ${error}`); 143 | }); 144 | } 145 | 146 | function handleGoogleImage(bot, message) { 147 | let params = { 148 | hl : 'en', 149 | q : message.text, 150 | start : 0, 151 | num : 1, 152 | tbm : 'isch', 153 | source : 'lnms', 154 | sa : 'X', 155 | biw : 1920, 156 | bih : 937 157 | }; 158 | axios.get(google_search_url + querystring.stringify(params), { 159 | headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' } 160 | }) 161 | .then(async response => { 162 | let result = await extractGoogleResults(response.data, extractGoogleImageResultFinal); 163 | bot.respond(!result ? 'I didn\'t find anything' : { attachment: { data: result, mimetype: 'image/jpeg' } }); 164 | }) 165 | .catch(error => { 166 | bot.error(`Sorry, I\'m having trouble contacting Google right now. ${error}`); 167 | }); 168 | } 169 | 170 | export default function(bot) { 171 | bot.command('google', handleGoogle); 172 | bot.command('gimg', handleGoogleImage); 173 | } 174 | -------------------------------------------------------------------------------- /plugins/imaging.js: -------------------------------------------------------------------------------- 1 | const sharp = require('sharp'); 2 | const { Image, createCanvas } = require('canvas'); 3 | 4 | async function getSupportedImageBuffer(image, mimetype) { 5 | if (mimetype === 'image/png') { 6 | return Buffer.from(image, 'base64'); 7 | } 8 | return await sharp(Buffer.from(image, 'base64')) 9 | .ensureAlpha() 10 | .png() 11 | .toBuffer(); 12 | } 13 | 14 | async function convertImageBufferBack(buffer, destMimetype) { 15 | if (destMimetype === 'image/png') { 16 | return buffer; 17 | } 18 | 19 | let sharpInstance = sharp(buffer); 20 | 21 | if (destMimetype === 'image/webp') { 22 | sharpInstance = sharpInstance.webp(); 23 | } 24 | else if (destMimetype === 'image/jpeg') { 25 | sharpInstance = sharpInstance.jpeg(); 26 | } 27 | 28 | return await sharpInstance.toBuffer(); 29 | } 30 | 31 | async function getImageMetadata(buffer) { 32 | return await sharp(buffer).metadata(); 33 | } 34 | 35 | async function getAttachmentData(bot, attachment) { 36 | let dataSplit = attachment.data.split(','); 37 | let mimetype = dataSplit[0].match(/:(.*?);/)[1]; 38 | let data = dataSplit[1]; 39 | 40 | let buffer = await getSupportedImageBuffer(data, mimetype); 41 | if (!buffer) { 42 | bot.error('Could not load specified image.'); 43 | return; 44 | } 45 | let metadata = await getImageMetadata(buffer); 46 | if (!metadata) { 47 | bot.error('Could not read metadata from image.'); 48 | return; 49 | } 50 | 51 | return { 52 | buffer, 53 | metadata, 54 | mimetype 55 | }; 56 | } 57 | 58 | function loadCanvasImage(buffer) { 59 | return new Promise((resolve, reject) => { 60 | let canvasImage = new Image(); 61 | canvasImage.onload = () => 62 | { 63 | resolve(canvasImage); 64 | }; 65 | canvasImage.onerror = err => { 66 | reject('Could not load image into canvas.'); 67 | }; 68 | canvasImage.src = buffer; 69 | }); 70 | } 71 | 72 | async function convertToPng(bot, message) { 73 | if (!message.attachment || !message.attachment.data) { 74 | bot.error('No image provided.'); 75 | return; 76 | } 77 | 78 | const dataSplit = message.attachment.data.split(','); 79 | const mimetype = dataSplit[0].match(/:(.*?);/)[1]; 80 | const data = dataSplit[1]; 81 | 82 | let sharpData = sharp(Buffer.from(data, 'base64')); 83 | 84 | const metadata = await sharpData.metadata(); 85 | 86 | const width = metadata.width; 87 | const height = metadata.height; 88 | 89 | sharpData = sharpData 90 | .ensureAlpha() 91 | .png(); 92 | 93 | if (message.text.length > 0) { 94 | const multiplier = parseInt(message.text); 95 | sharpData = sharpData 96 | .resize(multiplier * width, multiplier.height); 97 | } 98 | 99 | const resultBuffer = await sharpData.toBuffer(); 100 | 101 | bot.respond({ attachment: { 102 | data: `data:${mimetype};base64,${resultBuffer.toString('base64')}`, 103 | mimetype: 'image/png', 104 | type: "image" 105 | }}); 106 | } 107 | 108 | 109 | let subjectImage = {}; 110 | 111 | async function setSubject(bot, message) { 112 | if (!message.attachment || !message.attachment.data) { 113 | bot.error('No image provided.'); 114 | return; 115 | } 116 | 117 | subjectImage[message.chat.id] = message.attachment; 118 | 119 | bot.respond("I've stored the subject image."); 120 | } 121 | 122 | async function combine(bot, message, position) { 123 | if (!message.attachment || !message.attachment.data) { 124 | bot.error('No image provided.'); 125 | return; 126 | } 127 | if (!subjectImage[message.chat.id]) { 128 | bot.error('No subject image set - use "subject" command first.'); 129 | return; 130 | } 131 | 132 | let data = await getAttachmentData(bot, message.attachment); 133 | if (!data) { 134 | return; 135 | } 136 | 137 | let subjectData = await getAttachmentData(bot, subjectImage[message.chat.id]); 138 | if (!subjectData) { 139 | return; 140 | } 141 | 142 | const canvas = createCanvas(data.metadata.width + subjectData.metadata.width, 143 | Math.max(data.metadata.height, subjectData.metadata.height)); 144 | const ctx = canvas.getContext('2d'); 145 | const image = await loadCanvasImage(data.buffer); 146 | const subject = await loadCanvasImage(subjectData.buffer); 147 | 148 | ctx.drawImage(subject, 149 | 0, canvas.height / 2 - subject.height / 2); 150 | 151 | ctx.drawImage(image, 152 | canvas.width - image.width, 153 | canvas.height / 2 - image.height / 2); 154 | 155 | let mimetype = "image/png"; 156 | var imageData = await convertImageBufferBack(canvas.toBuffer(), mimetype); 157 | 158 | bot.respond({ attachment: { 159 | data: `data:${mimetype};base64,${imageData.toString('base64')}`, 160 | mimetype: mimetype, 161 | type: "image" 162 | }}); 163 | } 164 | 165 | async function addText(bot, message, position) { 166 | if (!message.attachment || !message.attachment.data) { 167 | bot.error('No image provided.'); 168 | return; 169 | } 170 | if (!message.text || message.text.length === 0) { 171 | bot.error('No text provided.'); 172 | return; 173 | } 174 | 175 | let data = await getAttachmentData(bot, message.attachment); 176 | if (!data) { 177 | return; 178 | } 179 | 180 | let isUpPosition = position !== "d"; 181 | 182 | const canvas = createCanvas(data.metadata.width, data.metadata.height); 183 | const ctx = canvas.getContext('2d'); 184 | const canvasImage = new Image(); 185 | canvasImage.onload = async () => 186 | { 187 | try { 188 | ctx.drawImage(canvasImage, 0, 0); 189 | 190 | let textHeight = Math.round(data.metadata.height * 0.15); 191 | let textX = data.metadata.width / 2; 192 | let textY = isUpPosition ? 0 : data.metadata.height; 193 | ctx.font = `${textHeight}px Impact`; 194 | ctx.textAlign = "center"; 195 | ctx.textBaseline = isUpPosition ? "top" : "bottom"; 196 | ctx.strokeStyle = 'black'; 197 | ctx.lineWidth = 8; 198 | ctx.strokeText(message.text, textX, textY, data.metadata.width); 199 | ctx.fillStyle = "white"; 200 | ctx.fillText(message.text, textX, textY, data.metadata.width); 201 | 202 | var imageData = await convertImageBufferBack(canvas.toBuffer(), data.mimetype); 203 | 204 | bot.respond({ attachment: { 205 | data: `data:${data.mimetype};base64,${imageData.toString('base64')}`, 206 | mimetype: data.mimetype, 207 | type: message.attachment.type 208 | }}); 209 | } 210 | catch (err) { 211 | console.log(err); 212 | bot.error('Something wrong happened when drawing the text.'); 213 | } 214 | }; 215 | canvasImage.onerror = err => { 216 | bot.error('Could not load image into canvas.'); 217 | }; 218 | canvasImage.src = data.buffer; 219 | } 220 | 221 | async function crop(bot, message, direction) { 222 | if (!message.attachment || !message.attachment.data) { 223 | bot.error('No image provided.'); 224 | return; 225 | } 226 | 227 | let percent = !message.text || message.text.length === 0 ? 10 : Number.parseInt(message.text); 228 | percent /= 100; 229 | 230 | let data = await getAttachmentData(bot, message.attachment); 231 | if (!data) { 232 | return; 233 | } 234 | 235 | let offsetY = data.metadata.height * percent; 236 | let offsetX = data.metadata.width * percent; 237 | 238 | const canvas = createCanvas(data.metadata.width, data.metadata.height); 239 | const ctx = canvas.getContext('2d'); 240 | const canvasImage = new Image(); 241 | canvasImage.onload = async () => 242 | { 243 | try { 244 | let sx = 0; 245 | let sy = (direction === "v" || direction == "d") ? offsetY : 0; 246 | let sw = canvasImage.width - ((direction === "l" || direction === "r") ? offsetX : direction === "h" ? offsetX * 2 : 0); 247 | let sh = canvasImage.height - ((direction === "u" || direction === "d") ? offsetY : direction === "v" ? offsetY * 2 : 0); 248 | let x = (direction === "l" || direction == "h") ? offsetX : 0; 249 | let y = (direction === "u" || direction == "v") ? offsetY : 0; 250 | 251 | ctx.drawImage(canvasImage, sx, sy, sw, sh, x, y, sw, sh); 252 | 253 | var imageData = await convertImageBufferBack(canvas.toBuffer(), data.mimetype); 254 | 255 | bot.respond({ attachment: { 256 | data: `data:${data.mimetype};base64,${imageData.toString('base64')}`, 257 | mimetype: data.mimetype, 258 | type: message.attachment.type 259 | }}); 260 | } 261 | catch (err) { 262 | console.log(err); 263 | bot.error('Something wrong happened when drawing the image.'); 264 | } 265 | }; 266 | canvasImage.onerror = err => { 267 | bot.error('Could not load image into canvas.'); 268 | }; 269 | canvasImage.src = data.buffer; 270 | } 271 | 272 | async function addPadding(bot, message, direction) { 273 | if (!message.attachment || !message.attachment.data) { 274 | bot.error('No image provided.'); 275 | return; 276 | } 277 | 278 | let percent = !message.text || message.text.length === 0 ? 10 : Number.parseInt(message.text); 279 | percent /= 100; 280 | 281 | let data = await getAttachmentData(bot, message.attachment); 282 | if (!data) { 283 | return; 284 | } 285 | 286 | let offsetY = data.metadata.height * percent; 287 | let offsetX = data.metadata.width * percent; 288 | 289 | const canvas = createCanvas(data.metadata.width, data.metadata.height); 290 | const ctx = canvas.getContext('2d'); 291 | const canvasImage = new Image(); 292 | canvasImage.onload = async () => 293 | { 294 | try { 295 | let dw = canvasImage.width - (direction === "h" ? offsetX * 2 : 0); 296 | let dh = canvasImage.height - ((direction === "u" || direction === "d") ? offsetY : direction === "v" ? offsetY * 2 : 0); 297 | let x = direction === "h" ? offsetX : 0; 298 | let y = (direction === "u" || direction == "v") ? offsetY : 0; 299 | 300 | ctx.drawImage(canvasImage, 0, 0, canvasImage.width, canvasImage.height, x, y, dw, dh); 301 | 302 | var imageData = await convertImageBufferBack(canvas.toBuffer(), data.mimetype); 303 | 304 | bot.respond({ attachment: { 305 | data: `data:${data.mimetype};base64,${imageData.toString('base64')}`, 306 | mimetype: data.mimetype, 307 | type: message.attachment.type 308 | }}); 309 | } 310 | catch (err) { 311 | console.log(err); 312 | bot.error('Something wrong happened when drawing the image.'); 313 | } 314 | }; 315 | canvasImage.onerror = err => { 316 | bot.error('Could not load image into canvas.'); 317 | }; 318 | canvasImage.src = data.buffer; 319 | } 320 | 321 | export default function(bot) { 322 | bot.command('meme', async (bot, message) => await addText(bot, message, "u")); 323 | bot.command('memeu', async (bot, message) => await addText(bot, message, "u")); 324 | bot.command('memed', async (bot, message) => await addText(bot, message, "d")); 325 | bot.command('pad', async (bot, message) => { 326 | let param = message.text || ''; 327 | bot.pass(bot.copy(message).text(`padh ${param}`).pipe(`padv ${param}`)); 328 | }); 329 | bot.command('padu', async (bot, message) => await addPadding(bot, message, "u")); 330 | bot.command('padd', async (bot, message) => await addPadding(bot, message, "d")); 331 | bot.command('padv', async (bot, message) => await addPadding(bot, message, "v")); 332 | bot.command('padh', async (bot, message) => await addPadding(bot, message, "h")); 333 | bot.command('crop', async (bot, message) => { 334 | let param = message.text || ''; 335 | bot.pass(bot.copy(message).text(`croph ${param}`).pipe(`cropv ${param}`)); 336 | }); 337 | bot.command('cropu', async (bot, message) => await crop(bot, message, "u")); 338 | bot.command('cropd', async (bot, message) => await crop(bot, message, "d")); 339 | bot.command('cropv', async (bot, message) => await crop(bot, message, "v")); 340 | bot.command('croph', async (bot, message) => await crop(bot, message, "h")); 341 | bot.command('subject', async (bot, message) => await setSubject(bot, message)); 342 | bot.command('combine', async (bot, message) => await combine(bot, message, "r")); 343 | bot.command('png', (bot, message) => convertToPng(bot, message)); 344 | } 345 | -------------------------------------------------------------------------------- /plugins/livescore.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | const livescore_url = 'http://www.livescores.com'; 4 | 5 | function httpGet(url) { 6 | return axios.get(url, { 7 | headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' } 8 | }); 9 | } 10 | 11 | function httpGetLiveScore() { 12 | return httpGet(livescore_url); 13 | } 14 | 15 | function extractLiveScoreMatchFacts1($) { 16 | let results = ''; 17 | let category = $('.content').children().first('.row-tall'); 18 | if (!category) { 19 | return null; 20 | } 21 | results = category.next().text(); 22 | 23 | //category.next('.row-tall').first().nextUntil('.row-tall').each(function(i, elem) { 24 | category.nextAll('.md').each(function(i, elem) { 25 | $(this).children().each(function (i, elem) { 26 | results += '\n' + $(this).text(); 27 | if ($(this).find('.yellowcard').length > 0) { 28 | results += ' (yellow card)'; 29 | } 30 | else if ($(this).find('.goal').length > 0) { 31 | results += ' (goal)'; 32 | } 33 | else if ($(this).find('.redyellowcard').length > 0) { 34 | results += ' (yellow + red card)'; 35 | } 36 | else if ($(this).find('.redcard').length > 0) { 37 | results += ' (red card)'; 38 | } 39 | }); 40 | }); 41 | 42 | return results; 43 | } 44 | 45 | function extractLiveScoreMatchFacts2($) { 46 | let results = $('.row-tall').first().text(); 47 | 48 | $('.row.bt').first().nextUntil('.row.bt', '.row-gray').each(function(i, elem) { 49 | $(this).children().each(function (i, elem) { 50 | results += '\n' + $(this).text().trim(); 51 | if ($(this).find('.yellowcard').length > 0) { 52 | results += ' (yellow card)'; 53 | } 54 | else if ($(this).find('.goal').length > 0) { 55 | results += ' (goal)'; 56 | } 57 | else if ($(this).find('.redyellowcard').length > 0) { 58 | results += ' (yellow + red card)'; 59 | } 60 | else if ($(this).find('.redcard').length > 0) { 61 | results += ' (red card)'; 62 | } 63 | }); 64 | }); 65 | 66 | return results; 67 | } 68 | 69 | function extractLiveScoreMatchFacts(body) { 70 | const $ = cheerio.load(body); 71 | let results = extractLiveScoreMatchFacts1($); 72 | if (!results) { 73 | results = extractLiveScoreMatchFacts2($); 74 | } 75 | return results; 76 | } 77 | 78 | function extractLiveScoreMatchFinal(body, searchTerm) { 79 | return new Promise(async (resolve, reject) => { 80 | const $ = cheerio.load(body); 81 | const searchTermLower = searchTerm.toLowerCase(); 82 | let url = ''; 83 | 84 | $('.content > .row-gray').each(function(i, elem) { 85 | let text = $(this).text(); 86 | if (text.toLowerCase().includes(searchTermLower)) { 87 | let link = $(this).find('a'); 88 | if (link) { 89 | url = link.attr('href'); 90 | } 91 | else { 92 | resolve(text.replace('\n', ' ')); 93 | } 94 | return false; 95 | } 96 | }); 97 | 98 | if (url && url.length > 0) { 99 | let response = await httpGet(livescore_url + url); 100 | let matchFacts = extractLiveScoreMatchFacts(response.data); 101 | resolve(matchFacts); 102 | } 103 | else { 104 | resolve(null); 105 | } 106 | }); 107 | } 108 | 109 | function extractLiveScoreFinal(body, searchTerm) { 110 | return new Promise(function (resolve, reject) { 111 | const $ = cheerio.load(body); 112 | const searchTermLower = searchTerm.toLowerCase(); 113 | 114 | $('.content > .row-gray').each(function(i, elem) { 115 | let text = $(this).text(); 116 | if (text.toLowerCase().includes(searchTermLower)) { 117 | resolve(text.replace('\n', ' ')); 118 | return false; 119 | } 120 | }); 121 | 122 | resolve(null); 123 | }); 124 | } 125 | 126 | function extractLiveScoreAllFinal(body, searchTerm) { 127 | return new Promise(function (resolve, reject) { 128 | const $ = cheerio.load(body); 129 | const searchTermLower = searchTerm.toLowerCase(); 130 | 131 | $('.content > .row-tall').each(function(i, elem) { 132 | let text = $(this).text(); 133 | if (text.toLowerCase().includes(searchTermLower)) { 134 | let results = []; 135 | $(this).nextUntil('.row-tall', '.row-gray').each(function (i, elem) { 136 | results.push($(this).text().replace('\n', ' ').trim()); 137 | }); 138 | resolve(text + '\n' + results.join('\n')); 139 | return false; 140 | } 141 | }); 142 | 143 | resolve(null); 144 | }); 145 | } 146 | 147 | async function handleLiveScore(bot, message) { 148 | let searchTerm = message.text; 149 | if (searchTerm.length == 0) { 150 | bot.error('Please specify a team name!'); 151 | return; 152 | } 153 | try { 154 | let response = await httpGetLiveScore(); 155 | let result = await extractLiveScoreFinal(response.data, searchTerm); 156 | bot.respond(result || 'I didn\'t find anything'); 157 | } 158 | catch(error) { 159 | bot.error(`Sorry, I\'m having trouble contacting LiveScore right now. ${error}`); 160 | } 161 | } 162 | 163 | async function handleLiveScoreMatch(bot, message) { 164 | let searchTerm = message.text; 165 | if (searchTerm.length == 0) { 166 | bot.error('Please specify a team name!'); 167 | return; 168 | } 169 | try { 170 | let response = await httpGetLiveScore(); 171 | let result = await extractLiveScoreMatchFinal(response.data, searchTerm); 172 | bot.respond(result || 'I didn\'t find anything'); 173 | } 174 | catch(error) { 175 | bot.error(`Sorry, I\'m having trouble contacting LiveScore right now. ${error}`); 176 | } 177 | } 178 | 179 | async function handleLiveScoreAll(bot, message) { 180 | let searchTerm = message.text; 181 | if (searchTerm.length == 0) { 182 | bot.error('Please specify a category name!'); 183 | return; 184 | } 185 | try { 186 | let response = await httpGetLiveScore(); 187 | let result = await extractLiveScoreAllFinal(response.data, searchTerm); 188 | bot.respond(result || 'I didn\'t find anything'); 189 | } 190 | catch(error) { 191 | bot.error(`Sorry, I\'m having trouble contacting LiveScore right now. ${error}`); 192 | } 193 | } 194 | 195 | export default function(bot) { 196 | bot.command('livescore', handleLiveScore); 197 | bot.command('liveall', handleLiveScoreAll); 198 | bot.command('livem', handleLiveScoreMatch); 199 | } 200 | -------------------------------------------------------------------------------- /plugins/natural.js: -------------------------------------------------------------------------------- 1 | const dialogflow = require('dialogflow'); 2 | 3 | let sessionClient = null; 4 | 5 | export default function(bot) { 6 | bot.command('natural', async (bot, message) => { 7 | if (!sessionClient) { 8 | sessionClient = new dialogflow.SessionsClient(); 9 | } 10 | 11 | const sessionPath = sessionClient.sessionPath(process.env.GOOGLE_PROJECT_ID, message.chat.id); 12 | const request = { 13 | session: sessionPath, 14 | queryInput: { 15 | text: { 16 | text: message.text, 17 | languageCode: 'en-US', 18 | }, 19 | }, 20 | }; 21 | const responses = await sessionClient.detectIntent(request); 22 | if (responses.length > 0) { 23 | const result = responses[0].queryResult; 24 | bot.respond(result.fulfillmentText); 25 | } 26 | else { 27 | bot.respond("I didn't quite get that. What do you mean?"); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /plugins/poll.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , db = new Datastore({ filename: './data/poll.db', autoload: true }); 3 | 4 | db.ensureIndex({ fieldName: 'chatId' }, function (err) { 5 | }); 6 | 7 | function getPollResults(poll) { 8 | let result = ''; 9 | let totalVotes = poll.votes.length; 10 | for (const [i, c] of poll.choices.entries()) { 11 | let choiceId = i + 1; 12 | let voteCount = poll.votes.reduce((prev, curr) => prev + (curr.choiceId == choiceId ? 1 : 0), 0); 13 | let percent = totalVotes > 0 ? parseInt(voteCount / totalVotes * 100) : 0; 14 | result += `#${choiceId} ${c} -- ${percent}% (${voteCount} votes)\n`; 15 | } 16 | result += `Total votes: ${totalVotes}`; 17 | return result; 18 | } 19 | 20 | export default function(bot) { 21 | bot.command('poll', (bot, message) => { 22 | if (message.text.length === 0) { 23 | db.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 24 | if (doc) { 25 | bot.respond(`Current Poll: ${doc.text}\n\n${getPollResults(doc)}`); 26 | } 27 | else { 28 | bot.respond("There are no active polls at this moment."); 29 | } 30 | }); 31 | } 32 | else { 33 | let voteOnce = message.text.startsWith('-once'); 34 | if (voteOnce) { 35 | message.text = message.text.substring(5).trim(); 36 | if (message.text.length == 0) { 37 | bot.respond("Please specify a poll question."); 38 | } 39 | } 40 | 41 | db.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 42 | if (doc) { 43 | bot.respond("There's an active poll, close that first before starting a new one."); 44 | } 45 | else { 46 | db.insert({ 47 | chatId: message.chat.id, 48 | active: true, 49 | voteOnce, 50 | author: message.sender.id, 51 | text: message.text, 52 | choices: [], 53 | votes: [] 54 | }, (err, doc) => { 55 | if (err) { 56 | bot.error("Could not create new poll!"); 57 | } 58 | else { 59 | bot.respond(`Poll started: ${message.text}\n\nAdd choices using "choice" and vote using "vote". Close the poll using "pollclose".\n\nFor example: "choice Burgers" or "vote 2".`); 60 | } 61 | }); 62 | } 63 | }); 64 | } 65 | }); 66 | 67 | bot.command('pollclose', (bot, message) => { 68 | db.update({ chatId : message.chat.id, active: true }, {$set: {active: false}}, {returnUpdatedDocs:true}, function (err, numReplaced, doc) { 69 | if (numReplaced === 1) { 70 | bot.respond(`Poll: ${doc.text}\n\n${getPollResults(doc)}\n\nThis poll is closed!`); 71 | } 72 | else { 73 | bot.respond("There are no active polls at this moment."); 74 | } 75 | }); 76 | }); 77 | 78 | bot.command('choice', (bot, message) => { 79 | if (message.text.length === 0) { 80 | bot.error('Please specify the text for your choice.'); 81 | return; 82 | } 83 | db.update({ chatId : message.chat.id, active: true }, {$push: {choices: message.text}}, {returnUpdatedDocs:true}, function (err, numReplaced, doc) { 84 | if (numReplaced === 1) { 85 | bot.respond(`Added "${message.text}" as choice #${doc.choices.length}.`); 86 | } 87 | else { 88 | bot.respond("There are no active polls at this moment."); 89 | } 90 | }); 91 | }); 92 | 93 | bot.command('vote', (bot, message) => { 94 | let choiceId = parseInt(message.text); 95 | if (isNaN(choiceId)) { 96 | bot.error('Please specify the number of your choice.'); 97 | return; 98 | } 99 | 100 | db.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 101 | if (doc) { 102 | if (choiceId < 1 || choiceId > doc.choices.length) { 103 | bot.error('Please specify a valid choice number.'); 104 | return; 105 | } 106 | 107 | if (doc.votes.find(v => v.senderId === message.sender.id && v.choiceId === choiceId)) { 108 | bot.error("You've already voted for this choice."); 109 | return; 110 | } 111 | 112 | let notice = "Recorded your vote for"; 113 | 114 | if (doc.voteOnce) { 115 | let voteId = doc.votes.findIndex(v => v.senderId === message.sender.id); 116 | if (voteId >= 0) { 117 | notice = "Changed your vote to"; 118 | doc.votes.splice(voteId, 1); 119 | } 120 | } 121 | 122 | let vote = { 123 | senderId: message.sender.id, 124 | choiceId 125 | }; 126 | 127 | doc.votes.push(vote); 128 | 129 | db.update({ chatId : message.chat.id, active: true }, {$set: {votes: doc.votes}}, {}, function (err, numReplaced) { 130 | if (numReplaced > 0) { 131 | bot.respond(`${notice} "${doc.choices[choiceId - 1]}".`); 132 | } 133 | else { 134 | bot.respond("There are no active polls at this moment."); 135 | } 136 | }); 137 | } 138 | else { 139 | bot.respond("There are no active polls at this moment."); 140 | } 141 | }); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /plugins/pose.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , db = new Datastore({ filename: './data/posers.db', autoload: true }); 3 | 4 | db.ensureIndex({ fieldName: 'chatId' }, function (err) { 5 | }); 6 | 7 | export function findPoser(chatId) { 8 | return new Promise((resolve, reject) => { 9 | db.findOne({ chatId }, function (err, doc) { 10 | resolve(doc); 11 | }); 12 | }); 13 | } 14 | 15 | export default function(bot) { 16 | bot.use(async (bot, message, next) => { 17 | let poser = await findPoser(message.chat.id); 18 | if (poser) { 19 | message.chat.realId = message.chat.id; 20 | message.chat.id = poser.posingAs; 21 | } 22 | 23 | next(); 24 | }); 25 | 26 | bot.command('pose', (bot, message) => { 27 | if (!message.sender.isAdmin) { 28 | bot.error('You don\'t have permission to pose.'); 29 | return; 30 | } 31 | if (message.text.length == 0) { 32 | bot.error('Please specify the channel id to pose.'); 33 | return; 34 | } 35 | let chatId = message.chat.realId || message.chat.id; 36 | db.update({ chatId }, { chatId, posingAs: message.text }, { upsert: true }, function () { 37 | bot.respond(`This chat is now posing as another chat.`); 38 | }); 39 | }); 40 | 41 | bot.command('unpose', async (bot, message) => { 42 | if (!message.sender.isAdmin) { 43 | bot.error('You don\'t have permission to pose.'); 44 | return; 45 | } 46 | let chatId = message.chat.realId || message.chat.id; 47 | db.remove({ chatId }, { }, function (err, numRemoved) { 48 | if (numRemoved > 0) { 49 | bot.respond(`This chat is not posing anymore.`); 50 | } 51 | else { 52 | bot.error(`I was not able to stop this chat from posing.`); 53 | } 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /plugins/record.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb') 2 | , db = new Datastore({ filename: './data/recording.db', autoload: true }); 3 | 4 | db.ensureIndex({ fieldName: 'name' }, function (err) { 5 | }); 6 | 7 | function parse(str) { 8 | let pos = str.indexOf(' '); 9 | return (pos === -1) ? [str, ''] : [str.substr(0, pos), str.substr(pos + 1)]; 10 | } 11 | 12 | function handleRecord(bot, message) { 13 | let parsedText = parse(message.text); 14 | let name = parsedText[0]; 15 | if (name.length == 0) { 16 | bot.error('Usage: !record '); 17 | return; 18 | } 19 | let rest = parsedText[1]; 20 | if (rest.length == 0 && !message.attachment) { 21 | bot.error('Usage: !record '); 22 | return; 23 | } 24 | let doc = { 25 | name : name.toLowerCase(), 26 | chatId : message.chat.id, 27 | authorId: message.sender.id, 28 | text : rest, 29 | attachment : message.attachment 30 | }; 31 | db.update({ name, chatId : message.chat.id }, doc, { upsert : true }, function () { 32 | bot.respond(`I've recorded "${name}".`); 33 | }); 34 | } 35 | 36 | function handleForget(bot, message) { 37 | let name = message.text; 38 | if (name.length == 0) { 39 | bot.error('Usage: !forget '); 40 | return; 41 | } 42 | function onRemoveDone(err, numRemoved) { 43 | if (numRemoved > 0) { 44 | bot.respond(`I forgot "${name}".`); 45 | } 46 | else { 47 | bot.error(`I don't have the recording "${name}".`); 48 | } 49 | } 50 | 51 | if (message.sender.isMe) { 52 | db.remove({ name, chatId : message.chat.id }, {}, onRemoveDone); 53 | } 54 | else { 55 | db.findOne({ name, chatId : message.chat.id }, { authorId : 1 }, function (err, doc) { 56 | if (doc) { 57 | if (!doc.authorId || doc.authorId == message.sender.id) { 58 | db.remove(doc, {}, onRemoveDone); 59 | } 60 | else { 61 | bot.error(`You can't make me forget "${name}". It was recorded by someone else.`); 62 | } 63 | } 64 | else { 65 | bot.error(`I don't have the recording "${name}".`); 66 | } 67 | }); 68 | } 69 | } 70 | 71 | function handleRaw(bot, message, next) { 72 | if (message.text.length == 0) { 73 | next(); 74 | return; 75 | } 76 | 77 | db.findOne({ name : message.text.toLowerCase(), chatId : message.chat.id }, function (err, doc) { 78 | if (doc) { 79 | bot.respond({ text : doc.text, attachment : doc.attachment }); 80 | } 81 | else { 82 | next(); 83 | } 84 | }); 85 | } 86 | 87 | function handleRecordings(bot, message) { 88 | let search = message.text.toLowerCase(); 89 | 90 | function callback(err, docs) { 91 | if (docs.length == 0) { 92 | if (search.length > 0) { 93 | bot.respond(`There are no recordings that match "${search}".`); 94 | } 95 | else { 96 | bot.respond(`There are no recordings yet.`); 97 | } 98 | } 99 | else { 100 | bot.respond(docs.map(e => e.name).join(', ')); 101 | } 102 | } 103 | 104 | if (search.length > 0) { 105 | db.find({ chatId : message.chat.id, name : { $regex: new RegExp(search, "g") } }, { name : 1 }).sort({ name : 1 }).exec(callback); 106 | } 107 | else { 108 | db.find({ chatId : message.chat.id }, { name : 1 }).sort({ name : 1 }).exec(callback); 109 | } 110 | } 111 | 112 | 113 | export default function(bot) { 114 | bot.command('record', handleRecord); 115 | bot.command('forget', handleForget); 116 | bot.command('recordings', handleRecordings); 117 | bot.raw(handleRaw); 118 | } 119 | -------------------------------------------------------------------------------- /plugins/reminders.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const moment = require('moment'); 3 | const Datastore = require('nedb') 4 | , db = new Datastore({ filename: './data/reminder.db', autoload: true }); 5 | 6 | db.ensureIndex({ fieldName: 'chatId' }, function (err) { 7 | }); 8 | 9 | function parse(str) { 10 | let pos = str.indexOf(' '); 11 | return (pos === -1) ? [str, ''] : [str.substr(0, pos), str.substr(pos + 1)]; 12 | } 13 | 14 | function parseWhen(when) { 15 | var now = new Date(); 16 | var mom = moment().seconds(0); 17 | var matches = when.match(/\d+[y,M,w,d,h,m]/g); 18 | if (matches) { 19 | for (let m of matches) { 20 | let number = m.substr(0, m.length - 1); 21 | let unit = m.substr(m.length - 1, 1); 22 | mom.add(number, unit); 23 | } 24 | return mom.toDate(); 25 | } 26 | return null; 27 | } 28 | 29 | export default function(bot) { 30 | bot.command('remindme', (bot, message) => { 31 | let parsedText = parse(message.text); 32 | let when = parsedText[0]; 33 | if (when.length == 0) { 34 | bot.error('Usage: remindme '); 35 | return; 36 | } 37 | let what = parsedText[1]; 38 | if (what.length == 0 && !message.attachment) { 39 | bot.error('Usage: remindme '); 40 | return; 41 | } 42 | if (!message.callbackUrl || message.callbackUrl.length == 0) { 43 | bot.error('Sorry, your bot client does not support this!'); 44 | return; 45 | } 46 | 47 | let date = parseWhen(when); 48 | 49 | if (!date) { 50 | bot.error("You didn't specify a correct time for the reminder."); 51 | return; 52 | } 53 | 54 | db.insert({ 55 | chatId: message.chat.id, 56 | active: true, 57 | reminderDate: date, 58 | description: what, 59 | author: message.sender.id, 60 | text: message.text, 61 | attachment: message.attachment, 62 | callbackUrl: message.callbackUrl 63 | }, (err, doc) => { 64 | if (err) { 65 | bot.error("Could not create a new reminder!"); 66 | } 67 | else { 68 | bot.respond(`Ok, I'll remind you.`); 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | (function checkReminders() { 75 | var now = new Date(); 76 | 77 | db.findOne({ reminderDate: { $lte: now }, active: true }, (err, doc) => { 78 | if (doc) { 79 | db.update({ _id: doc._id }, { active: false }); 80 | 81 | let message = { 82 | chat: { id: doc.chatId }, 83 | text: `Reminder: ${doc.description}`, 84 | attachment: doc.attachment 85 | }; 86 | axios.post(doc.callbackUrl, message) 87 | .then(async response => { 88 | if (response.status == 200) { 89 | console.log(`Received back: ${JSON.stringify(response.data)}`); 90 | } 91 | }); 92 | } 93 | setTimeout(checkReminders, 60000); 94 | }); 95 | })(); 96 | -------------------------------------------------------------------------------- /plugins/remove_bg.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const crawl_url = 'https://www.remove.bg/upload'; 3 | const puppeteer = require('puppeteer-extra'); 4 | const path = require('path'); 5 | 6 | const pluginStealth = require('puppeteer-extra-plugin-stealth'); 7 | puppeteer.use(pluginStealth()); 8 | 9 | const pluginRecaptcha = require('puppeteer-extra-plugin-recaptcha'); 10 | puppeteer.use(pluginRecaptcha({ 11 | provider: {id: '2captcha', token: process.env.CAPTCHA_SOLVER_TOKEN}, 12 | visualFeedback: true 13 | })); 14 | 15 | var browser; 16 | 17 | const DEFAULT_CHROMIUM_ARGS = [ 18 | //"--disable-gpu", 19 | "--renderer", 20 | "--no-sandbox", 21 | "--no-service-autorun", 22 | "--no-experiments", 23 | "--no-default-browser-check", 24 | //"--disable-webgl", 25 | "--disable-threaded-animation", 26 | "--disable-threaded-scrolling", 27 | "--disable-in-process-stack-traces", 28 | "--disable-histogram-customizer", 29 | //"--disable-gl-extensions", 30 | "--disable-extensions", 31 | "--disable-composited-antialiasing", 32 | //"--disable-canvas-aa", 33 | "--disable-3d-apis", 34 | //"--disable-accelerated-2d-canvas", 35 | //"--disable-accelerated-jpeg-decoding", 36 | "--disable-accelerated-mjpeg-decode", 37 | "--disable-app-list-dismiss-on-blur", 38 | "--disable-accelerated-video-decode", 39 | //"--num-raster-threads=1", 40 | ]; 41 | 42 | var uniqueId = 0; 43 | 44 | async function saveImageToFile(image) { 45 | return new Promise((resolve, reject) => { 46 | let data = image.data.split(',')[1]; 47 | let fileName = require("path").join(process.cwd(), `./temp/removebg-work-${uniqueId++}.jpg`); 48 | if (uniqueId > 10000) { 49 | uniqueId = 0; 50 | } 51 | require("fs").writeFile(fileName, data, 'base64', function(err) { 52 | if (err) { 53 | reject(err); 54 | } 55 | else { 56 | resolve(fileName); 57 | } 58 | }); 59 | }); 60 | } 61 | 62 | async function getImageData(url) { 63 | var res = await axios.get(url, { responseType: 'arraybuffer' }); 64 | let data = Buffer.from(res.data, 'binary').toString('base64'); 65 | let mimetype = res.headers['content-type']; 66 | let image = { 67 | data: `data:${mimetype};base64,${data}`, 68 | type: 'image', 69 | mimetype: mimetype 70 | }; 71 | return image; 72 | } 73 | 74 | 75 | async function removeImageBackground(image) { 76 | if (!browser) { 77 | browser = await puppeteer.launch({ 78 | headless: true, 79 | userDataDir: path.resolve("./temp/rb_user_data"), 80 | args: DEFAULT_CHROMIUM_ARGS, 81 | ignoreHTTPSErrors: true, 82 | devtools: false, 83 | defaultViewport: null 84 | }); 85 | } 86 | 87 | await browser.pages(); 88 | 89 | let page = await browser.newPage(); 90 | await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'); 91 | await page.setBypassCSP(true); 92 | await page.goto(crawl_url, { 93 | waitUntil: 'networkidle0', 94 | timeout: 0 95 | }); 96 | 97 | await page.waitFor('button.btn-primary.btn-upload', {timeout: 8000}); 98 | 99 | let imageFileName = await saveImageToFile(image); 100 | 101 | try { 102 | const [fileChooser] = await Promise.all([ 103 | page.waitForFileChooser(), 104 | page.click('button.btn-primary.btn-upload'), 105 | ]); 106 | await fileChooser.accept([imageFileName]); 107 | 108 | await page.waitFor(3000); 109 | 110 | const { captchas, solutions, solved, error } = await page.solveRecaptchas(); 111 | 112 | if (error && error.length > 0) { 113 | throw "Could not solve captcha!"; 114 | } 115 | 116 | await page.waitFor(2000); 117 | 118 | await page.waitFor('div.my-3 a.btn-primary', {timeout: 8000}); 119 | 120 | try { 121 | const imageUrl = await page.$eval('div.my-3 a.btn-primary', a => a.getAttribute('href')); 122 | return await getImageData(imageUrl); 123 | } 124 | finally { 125 | await page.click('a.btn.image-result--delete-btn'); 126 | 127 | await page.waitFor(3000); 128 | } 129 | } 130 | finally { 131 | require("fs").unlink(imageFileName, (err) => {}); 132 | 133 | await page.close(); 134 | } 135 | } 136 | 137 | async function handleRemoveBg(bot, message) { 138 | if (!message.attachment) { 139 | bot.error('No image provided!'); 140 | return; 141 | } 142 | try { 143 | let result = await removeImageBackground(message.attachment); 144 | if (result) { 145 | bot.respond(bot.new().attachment(result)); 146 | } 147 | else { 148 | bot.error('I could not process this image.'); 149 | } 150 | } 151 | catch (error) { 152 | console.log(error.stack); 153 | bot.error('I could not process this image at this time.'); 154 | } 155 | } 156 | 157 | export default function(bot) { 158 | bot.command('rembg', handleRemoveBg); 159 | } 160 | -------------------------------------------------------------------------------- /plugins/sticker.js: -------------------------------------------------------------------------------- 1 | const sharp = require('sharp'); 2 | 3 | async function stickerize(bot, message, removeBg, trim) { 4 | if (!message.attachment || !message.attachment.data) { 5 | bot.error('No image provided.'); 6 | return; 7 | } 8 | 9 | let processed = message; 10 | 11 | if (removeBg) { 12 | processed = await bot.receive(bot.copy(message).text('rembg')); 13 | if (!processed || !processed.attachment || !processed.attachment.data) { 14 | processed = message; 15 | } 16 | } 17 | 18 | let fit = message.text === "fill" ? sharp.fit.fill : sharp.fit.cover; 19 | let position = fit === sharp.fit.cover ? sharp.gravity.north : sharp.gravity.center; 20 | 21 | let data = processed.attachment.data.split(',')[1]; 22 | let sharpInstance = sharp(Buffer.from(data, 'base64')).ensureAlpha(); 23 | 24 | if (trim) { 25 | sharpInstance = sharpInstance.trim(5); 26 | } 27 | 28 | let stickerData = await sharpInstance 29 | .resize(512, 512, { 30 | fit, 31 | position, 32 | background: { r: 255, g: 255, b: 255, alpha: 0 } 33 | }) 34 | .webp() 35 | .toBuffer(); 36 | 37 | bot.respond({ attachment: { 38 | data: `data:image/webp;base64,${stickerData.toString('base64')}`, 39 | mimetype: 'image/webp', 40 | type: 'sticker' 41 | }}); 42 | } 43 | 44 | export default function(bot) { 45 | bot.command('stickerize', async (bot, message) => { 46 | await stickerize(bot, message, true, true) 47 | }); 48 | bot.command('faststicker', async (bot, message) => { 49 | await stickerize(bot, message, false, true) 50 | }); 51 | bot.command('baresticker', async (bot, message) => { 52 | await stickerize(bot, message, false, false) 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /plugins/tell.js: -------------------------------------------------------------------------------- 1 | function parse(str) { 2 | let pos = str.indexOf(' '); 3 | return (pos === -1) ? [str, ''] : [str.substr(0, pos), str.substr(pos + 1)]; 4 | }; 5 | 6 | export default function(bot) { 7 | bot.command('tell', async (bot, message) => { 8 | let parsedText = parse(message.text); 9 | bot.pass(bot.copy(message) 10 | .text(parsedText[1]) 11 | .addressee(parsedText[0])); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /plugins/translate.js: -------------------------------------------------------------------------------- 1 | const {Translate} = require('@google-cloud/translate'); 2 | 3 | const translater = new Translate({projectId: process.env.GOOGLE_PROJECT_ID}); 4 | 5 | function parse(str) { 6 | let pos = str.indexOf(' '); 7 | return (pos === -1) ? [str, ''] : [str.substr(0, pos), str.substr(pos + 1)]; 8 | }; 9 | 10 | async function translate(bot, message) { 11 | let parsedText = parse(message.text); 12 | let target = parsedText[0].toLowerCase(); 13 | if (target.length == 0) { 14 | bot.error('Usage: translate '); 15 | return; 16 | } 17 | let rest = parsedText[1]; 18 | if (rest.length == 0) { 19 | bot.error('Usage: translate '); 20 | return; 21 | } 22 | 23 | try { 24 | const [translation] = await translater.translate(rest, target); 25 | bot.respond(`${translation}`); 26 | } 27 | catch (err) { 28 | console.log(err); 29 | bot.error('I couldn\'t translate that.'); 30 | } 31 | } 32 | 33 | export default function(bot) { 34 | bot.command('translate', translate); 35 | bot.command('tran', translate); 36 | } 37 | -------------------------------------------------------------------------------- /plugins/trivia.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const axios = require('axios'); 3 | const Datastore = require('nedb') 4 | , questions = new Datastore({ filename: './data/trivia_questions.db', autoload: true }) 5 | , sessions = new Datastore({ filename: './data/trivia_sessions.db', autoload: true }); 6 | 7 | questions.ensureIndex({ fieldName: 'category' }, function (err) { 8 | }); 9 | 10 | sessions.ensureIndex({ fieldName: 'chatId' }, function (err) { 11 | }); 12 | 13 | function sendCallbackMessage(doc, text) { 14 | let message = { 15 | chat: { id: doc.chatId }, 16 | text 17 | }; 18 | axios.post(doc.callbackUrl, message) 19 | .then(async response => { 20 | if (response.status == 200) { 21 | console.log(`Received back: ${JSON.stringify(response.data)}`); 22 | } 23 | }) 24 | .catch(err => { 25 | console.log(err); 26 | }); 27 | } 28 | 29 | function getTriviaResults(doc) { 30 | if (!doc.participants || doc.participants.length === 0) { 31 | return 'Nobody participated in this game.'; 32 | } 33 | 34 | let results = []; 35 | for (let participant of doc.participants) { 36 | results.push({ 37 | participant, 38 | score: doc.questions.reduce((prev, curr) => 39 | prev + (curr.answeredBy && curr.answeredBy.id == participant.id ? 1 : 0), 0) 40 | }); 41 | } 42 | results.sort((a, b) => b.score - a.score); 43 | 44 | let text = ''; 45 | let index = 1; 46 | for (let result of results) { 47 | text += `${index}. ${result.participant.name || result.participant.id} => ${result.score}\n`; 48 | index++; 49 | } 50 | 51 | if (results.length === 1 || results[0].score !== results[1].score) { 52 | text += `${results[0].participant.name || results[0].participant.id} ${doc.active ? 'is winning' : 'won'}!`; 53 | } 54 | else { 55 | text += `It's ${doc.active ? 'currently ' : ''}a tie!`; 56 | } 57 | 58 | return text; 59 | } 60 | 61 | function addParticipant(doc, sender) { 62 | doc.participants = doc.participants || []; 63 | if (doc.participants.findIndex(p => p.id == sender.id) < 0) { 64 | doc.participants.push({ 65 | id: sender.id, 66 | name: sender.name 67 | }); 68 | } 69 | } 70 | 71 | function checkAnswer(bot, doc, message) { 72 | addParticipant(doc, message.sender); 73 | 74 | let answer = message.text.trim().toLowerCase(); 75 | let question = doc.questions[doc.questions.length - 1]; 76 | if (question.answers.findIndex(a => a.trim().toLowerCase() == answer) > -1) { 77 | question.isDone = true; 78 | question.answeredBy = { 79 | id: message.sender.id, 80 | name: message.sender.name, 81 | answer: message.text 82 | }; 83 | doc.nextStateDate = new Date(new Date().getTime() + 2000); 84 | saveSession(doc); 85 | 86 | const greeting = _.sample(['Good', 'Great', 'Awesome', 'Nice', 'Excellent']); 87 | bot.respond(`${greeting}! The correct answer "${message.text}" was given by ${message.sender.name || message.sender.id}.`); 88 | } 89 | else { 90 | saveSession(doc); 91 | } 92 | } 93 | 94 | function saveSession(doc) { 95 | sessions.update({ _id: doc._id }, doc); 96 | } 97 | 98 | function inactivateSession(doc) { 99 | doc.active = false; 100 | saveSession(doc); 101 | } 102 | 103 | function hasActiveQuestion(doc) { 104 | return doc.questions.length > 0 && !doc.questions[doc.questions.length - 1].isDone; 105 | } 106 | 107 | function handleTimeout(doc) { 108 | let question = doc.questions[doc.questions.length - 1]; 109 | doc.nextStateDate = new Date(new Date().getTime() + 3000); 110 | question.isDone = true; 111 | saveSession(doc); 112 | 113 | sendCallbackMessage(doc, `Time's out! Nobody gets points for this question.`); 114 | } 115 | 116 | function handleGameComplete(doc) { 117 | inactivateSession(doc); 118 | sendCallbackMessage(doc, `The game has ended after ${doc.questions.length} questions!\n\n${getTriviaResults(doc)}`); 119 | } 120 | 121 | function startNextQuestion(doc) { 122 | let query = {}; 123 | if (doc.questions.length > 0) { 124 | query._id = { $nin: doc.questions.map(q => q.questionId) }; 125 | } 126 | if (doc.category) { 127 | query.category = doc.category; 128 | } 129 | questions.count(query, function (err, count) { 130 | if (count === 0) { 131 | inactivateSession(doc); 132 | sendCallbackMessage(doc, `Could not retrieve a question for this trivia. Game over.\n\n${getTriviaResults(doc)}`); 133 | return; 134 | } 135 | 136 | let index = Math.floor(Math.random() * Math.floor(count)); 137 | 138 | questions.find(query) 139 | .skip(index) 140 | .limit(1) 141 | .exec(function (err, docs) { 142 | if (err || docs.length === 0) { 143 | inactivateSession(doc); 144 | sendCallbackMessage(doc, `Could not retrieve a question for this trivia. Game over.\n\n${getTriviaResults(doc)}`); 145 | return; 146 | } 147 | let question = docs[0]; 148 | doc.questions.push({ 149 | questionId: question._id, 150 | text: question.text, 151 | answers: question.answers, 152 | isDone: false 153 | }); 154 | doc.nextStateDate = new Date(new Date().getTime() + 30000); 155 | saveSession(doc); 156 | 157 | sendCallbackMessage(doc, `Question #${doc.questions.length}: [${question.category}] ${question.text}`); 158 | }); 159 | }); 160 | } 161 | 162 | export default function(bot) { 163 | bot.use((bot, message, next) => { 164 | if (message.text && (message.text.toLowerCase() === "triviastop" || message.text.toLowerCase() === "trivia")) { 165 | next(); 166 | return; 167 | } 168 | 169 | sessions.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 170 | if (!doc) { 171 | next(); 172 | } 173 | else if (hasActiveQuestion(doc)) { 174 | checkAnswer(bot, doc, message); 175 | } 176 | else { 177 | bot.error("I haven't asked anything yet, take it easy!"); 178 | } 179 | }); 180 | }); 181 | 182 | bot.command('trivia', (bot, message) => { 183 | sessions.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 184 | if (doc) { 185 | bot.respond(`Current Trivia:\n\n${getTriviaResults(doc)}`); 186 | } 187 | else { 188 | bot.respond("There is no active trivia at this moment."); 189 | } 190 | }); 191 | }); 192 | 193 | bot.command('triviastart', (bot, message) => { 194 | if (!message.callbackUrl || message.callbackUrl.length == 0) { 195 | bot.error('Sorry, your bot client does not support this!'); 196 | } 197 | else { 198 | sessions.findOne({ chatId : message.chat.id, active: true }, function (err, doc) { 199 | if (doc) { 200 | bot.respond("There's an active trivia, stop that first before starting a new one."); 201 | } 202 | else { 203 | sessions.insert({ 204 | chatId: message.chat.id, 205 | active: true, 206 | author: message.sender.id, 207 | nextStateDate: new Date(new Date().getTime() + 5000), 208 | category: message.text.toLowerCase(), 209 | callbackUrl: message.callbackUrl, 210 | questions: [], 211 | participants: [] 212 | }, (err, doc) => { 213 | if (err) { 214 | bot.error("Could not start a new trivia game!"); 215 | } 216 | else if (doc.category) { 217 | bot.respond(`Trivia started for category "${message.text}" - Get ready!`); 218 | } 219 | else { 220 | bot.respond(`Trivia started for all categories - Get ready!`); 221 | } 222 | }); 223 | } 224 | }); 225 | } 226 | }); 227 | 228 | bot.command('triviastop', (bot, message) => { 229 | sessions.update({ chatId : message.chat.id, active: true }, {$set: {active: false}}, {returnUpdatedDocs:true}, function (err, numReplaced, doc) { 230 | if (numReplaced === 1) { 231 | bot.respond(`Trivia Results:\n\n${getTriviaResults(doc)}\n\nThe game is over!`); 232 | } 233 | else { 234 | bot.respond("There are no active trivias at this moment."); 235 | } 236 | }); 237 | }); 238 | 239 | bot.command('triviaq', (bot, message) => { 240 | if (!message.text || message.text.length === 0) { 241 | bot.error('Please specify a question in this format: ``'); 242 | return; 243 | } 244 | let tokens = message.text.split("`", 3); 245 | if (tokens.length !== 3) { 246 | bot.error('Please specify a question in this format: ``'); 247 | return; 248 | } 249 | 250 | let category = tokens[0].trim(); 251 | let text = tokens[1].trim(); 252 | let answer = tokens[2].trim(); 253 | 254 | if (category.length === 0 || text.length === 0) { 255 | bot.error('Please specify a category and the question text.'); 256 | return; 257 | } 258 | 259 | let query = { 260 | category: { $regex: new RegExp(category.split(' ').join('|'), 'i') }, 261 | text: { $regex: new RegExp(text.split(' ').join('|'), 'i') }, 262 | answer: { $regex: new RegExp(answer.split(' ').join('|'), 'i') } 263 | }; 264 | 265 | let doc = { 266 | author: message.sender.id, 267 | authorName: message.sender.name, 268 | category, 269 | text, 270 | answers: [answer] 271 | }; 272 | 273 | questions.update(query, doc, { upsert: true }, (err, doc) => { 274 | if (err) { 275 | bot.error("Could not create a new question!"); 276 | } 277 | else { 278 | bot.respond(`Saved your question successfully.`); 279 | } 280 | }); 281 | }); 282 | } 283 | 284 | (function checkSessions() { 285 | var now = new Date(); 286 | 287 | sessions.findOne({ nextStateDate: { $lte: now }, active: true }, (err, doc) => { 288 | try { 289 | if (doc) { 290 | if (hasActiveQuestion(doc)) { 291 | handleTimeout(doc); 292 | } 293 | else if (doc.questions.length === 10) { 294 | handleGameComplete(doc); 295 | } 296 | else { 297 | startNextQuestion(doc); 298 | } 299 | } 300 | } 301 | catch (err) { 302 | console.log("Error while handling next state for trivia."); 303 | console.log(err); 304 | } 305 | setTimeout(checkSessions, 1000); 306 | }); 307 | })(); 308 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { SuperBot } from './super-bot' 3 | import MessageBuilder from './super-bot/MessageBuilder'; 4 | 5 | function inspectMessage(msg) { 6 | return JSON.stringify(msg, function (key, value) { 7 | if (key === 'data' && typeof value === 'string' && value.length > 50) { 8 | return value.substring(0, 50) + '[...]'; 9 | } 10 | return value; 11 | }); 12 | } 13 | 14 | async function main() { 15 | console.log('Starting SuperBot...'); 16 | 17 | const config = require('config'); 18 | const bot = new SuperBot(config.get("SuperBot")); 19 | 20 | await bot.start(); 21 | 22 | const app = express(); 23 | app.use(express.json({ limit: '20mb' })); 24 | 25 | app.post('/message', async (req, res) => { 26 | console.log(`Received: ${inspectMessage(req.body)}`); 27 | 28 | try { 29 | const message = await bot.receive(req.body); 30 | 31 | console.log(`Sending: ${inspectMessage(message)}`); 32 | 33 | res.send(message); 34 | } 35 | catch (error) { 36 | console.log(error); 37 | 38 | const message = new MessageBuilder().error(error).build(); 39 | console.log(`Sending error: ${inspectMessage(message)}`); 40 | res.send(message); 41 | } 42 | }); 43 | 44 | const port = config.get('app.port') || 3000; 45 | app.listen(port, () => { 46 | console.log(`Listening on port ${port}`); 47 | }); 48 | } 49 | 50 | main(); -------------------------------------------------------------------------------- /src/super-bot/Message.js: -------------------------------------------------------------------------------- 1 | export default class Message { 2 | } -------------------------------------------------------------------------------- /src/super-bot/MessageBuilder.js: -------------------------------------------------------------------------------- 1 | import Message from './Message' 2 | 3 | export default class MessageBuilder { 4 | 5 | constructor(original) { 6 | this.original = original; 7 | this.message = new Message(); 8 | 9 | if (original && original.addressee) { 10 | this.message.addressee = original.addressee 11 | } 12 | } 13 | 14 | text(s) { 15 | this.message.text = s; 16 | return this; 17 | } 18 | 19 | pipe(s) { 20 | this.message.text = `${(this.message.text || '')} | ${s}`; 21 | return this; 22 | } 23 | 24 | attachment(a) { 25 | return this.attachments([a]); 26 | } 27 | 28 | attachments(a) { 29 | this.message.attachments = a; 30 | this.message.attachment = a && a[0]; 31 | return this; 32 | } 33 | 34 | error(s) { 35 | this.message.error = true; 36 | this.text(s); 37 | return this; 38 | } 39 | 40 | raw(data) { 41 | Object.assign(this.message, data); 42 | return this; 43 | } 44 | 45 | addressee(value) { 46 | this.message.addressee = value; 47 | return this; 48 | } 49 | 50 | build() { 51 | return this.message; 52 | } 53 | } -------------------------------------------------------------------------------- /src/super-bot/Middleware.js: -------------------------------------------------------------------------------- 1 | export default class Middleware { 2 | 3 | constructor() { 4 | if (!Array.prototype.last) { 5 | Array.prototype.last = function() { 6 | return this[this.length - 1]; 7 | } 8 | } 9 | 10 | if (!Array.prototype.reduceOneRight) { 11 | Array.prototype.reduceOneRight = function() { 12 | return this.slice(0, -1); 13 | } 14 | } 15 | } 16 | 17 | use(fn) { 18 | this.go = (stack => (...args) => stack(...args.reduceOneRight(), () => { 19 | let _next = args.last(); 20 | fn.apply(this, [...args.reduceOneRight(), _next.bind.apply(_next, [null, ...args.reduceOneRight()])]); 21 | }))(this.go); 22 | } 23 | 24 | go(...args) { 25 | let _next = args.last(); 26 | _next.apply(this, args.reduceOneRight()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/super-bot/SuperBot.js: -------------------------------------------------------------------------------- 1 | import Middleware from './Middleware' 2 | import Message from './Message' 3 | import MessageBuilder from './MessageBuilder' 4 | import SuperBotProxy from './SuperBotProxy' 5 | 6 | function parse(str) { 7 | let pos = str.indexOf(' '); 8 | return (pos === -1) ? [str, ''] : [str.substr(0, pos), str.substr(pos + 1)]; 9 | }; 10 | 11 | function join(str1, str2, delim) { 12 | if (str1.length == 0) { 13 | return str2; 14 | } 15 | else if (str2.length == 0) { 16 | return str1; 17 | } 18 | else { 19 | return str1 + delim + str2; 20 | } 21 | } 22 | 23 | async function loadPlugin(bot, path, file) { 24 | try { 25 | const pluginPath = require('path').join(path, file); 26 | const mod = await import(pluginPath); 27 | 28 | console.log(`Initializing plugin: ${file}...`); 29 | 30 | mod.default(bot); 31 | 32 | console.log(`Done plugin: ${file}.`); 33 | } 34 | catch(err) { 35 | console.error(err); 36 | } 37 | } 38 | 39 | async function loadAllPlugins(bot, path) { 40 | console.log(`Loading all plugins from path: ${path}`); 41 | for (const file of require('fs').readdirSync(path)) { 42 | console.log(`Loading plugin: ${file}...`); 43 | await loadPlugin(bot, path, file); 44 | } 45 | } 46 | 47 | async function loadPlugins(bot, path, plugins) { 48 | console.log(`Loading plugins from path: ${path}`); 49 | if (plugins) { 50 | for (let plugin of plugins) { 51 | if (plugin.disabled) continue; 52 | 53 | console.log(`Loading plugin: ${plugin.name}.js...`); 54 | await loadPlugin(bot, path, `${plugin.name}.js`); 55 | } 56 | } 57 | else { 58 | await loadAllPlugins(bot, path); 59 | } 60 | } 61 | 62 | String.prototype.format = String.prototype.format || 63 | function () { 64 | "use strict"; 65 | var str = this.toString(); 66 | if (arguments.length) { 67 | var t = typeof arguments[0]; 68 | var key; 69 | var args = ("string" === t || "number" === t) ? 70 | Array.prototype.slice.call(arguments) 71 | : arguments[0]; 72 | 73 | for (key in args) { 74 | str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]); 75 | } 76 | } 77 | 78 | return str; 79 | }; 80 | 81 | export default class SuperBot { 82 | constructor(options) { 83 | this.options = options; 84 | this.commands = {}; 85 | this.middleware = new Middleware(); 86 | this.rawMiddleware = new Middleware(); 87 | } 88 | 89 | async start() { 90 | if (this._started) return; 91 | this._started = true; 92 | 93 | this.command('all', (bot, message) => { 94 | bot.respond(Object.getOwnPropertyNames(this.commands).join(', ')); 95 | }); 96 | 97 | if (this.options.pluginsPath) { 98 | await loadPlugins( 99 | this, 100 | require('path').join(__dirname, '../../', this.options.pluginsPath), 101 | this.options.plugins); 102 | } 103 | } 104 | 105 | use(fn) { 106 | this.middleware.use(fn); 107 | return this; 108 | } 109 | 110 | command(command, handler) { 111 | command = command.toLowerCase(); 112 | if (this.commands[command]) { 113 | throw new Error(`Command already registered: ${command}`); 114 | } 115 | this.commands[command] = handler; 116 | } 117 | 118 | raw(handler) { 119 | this.rawMiddleware.use(handler); 120 | } 121 | 122 | _normalizeIncoming(message) { 123 | if (message.attachment && (!message.attachments || message.attachments.length == 0)) { 124 | message.attachments = [message.attachment]; 125 | } 126 | else if (!message.attachment && message.attachments && message.attachments.length > 0) { 127 | message.attachment = message.attachments[0]; 128 | } 129 | return message; 130 | } 131 | 132 | _normalizeOutgoing(message) { 133 | if (message.attachment) { 134 | if (!message.attachments || message.attachments.length == 0) { 135 | message.attachments = [message.attachment]; 136 | } 137 | message.attachment = undefined; 138 | } 139 | return message; 140 | } 141 | 142 | _handleMessage(message) { 143 | this._normalizeIncoming(message); 144 | 145 | return new Promise((resolve, reject) => { 146 | try { 147 | const respondHandler = (message) => { 148 | resolve(message); 149 | }; 150 | this.middleware.go(new SuperBotProxy(this, message, respondHandler), message, (b, message) => { 151 | let parsedText = parse(message.text); 152 | let bot = new SuperBotProxy(this, message, respondHandler); 153 | 154 | let first = parsedText[0].toLowerCase(); 155 | if (first.length === 0) { 156 | bot.error('I don\'t understand that.'); 157 | return; 158 | } 159 | 160 | try { 161 | if (this.commands[first]) { 162 | let command = first; 163 | message.command = command; 164 | message.fullText = message.text; 165 | message.text = parsedText[1]; 166 | 167 | this.commands[first](bot, message); 168 | } 169 | else { 170 | this.rawMiddleware.go(bot, message, (b, message) => { 171 | bot.error(`I don't recognize "${first}".`); 172 | }); 173 | } 174 | } 175 | catch (err) { 176 | console.log(err); 177 | reject('Something went wrong.'); 178 | } 179 | }); 180 | } 181 | catch (err) { 182 | console.log(err); 183 | reject('Something went wrong.'); 184 | } 185 | }); 186 | } 187 | 188 | async receiveInternal(message) { 189 | if (this.options.enablePipe) { 190 | const pipeline = message.text.split(` ${this.options.pipeDelimeter || '|'} `); 191 | if (pipeline.length > 1) { 192 | message.text = pipeline[0]; 193 | } 194 | for (let i = 0; i < pipeline.length; ++i) { 195 | const result = await this._handleMessage(message); 196 | 197 | if (i === pipeline.length - 1) { 198 | return result; 199 | } 200 | else { 201 | message.text = join(pipeline[i + 1], result.text || "", " "); 202 | message.attachment = result.attachment; 203 | message.attachments = result.attachments; 204 | } 205 | } 206 | } 207 | else { 208 | return await this._handleMessage(message); 209 | } 210 | } 211 | 212 | async receive(message) { 213 | const response = await this.receiveInternal(message); 214 | return this._normalizeOutgoing(response); 215 | } 216 | } -------------------------------------------------------------------------------- /src/super-bot/SuperBotProxy.js: -------------------------------------------------------------------------------- 1 | import Message from './Message' 2 | import MessageBuilder from './MessageBuilder' 3 | 4 | export default class SuperBotProxy { 5 | 6 | constructor(bot, message, respondHandler) { 7 | this.bot = bot; 8 | this.message = message; 9 | this.respondHandler = respondHandler; 10 | } 11 | 12 | _sanitize(m) { 13 | if (typeof m === 'string') { 14 | m = this.new().text(m).build(); 15 | } 16 | else if (m instanceof MessageBuilder) { 17 | m = m.build(); 18 | } 19 | else if (!(m instanceof Message)) { 20 | m = this.new().raw(m).build(); 21 | } 22 | return m; 23 | } 24 | 25 | new() { 26 | return new MessageBuilder(this.message); 27 | } 28 | 29 | respond(m) { 30 | if (this.respondHandler) { 31 | this.respondHandler(this._sanitize(m)); 32 | } 33 | } 34 | 35 | async receive(m) { 36 | if (this.bot) { 37 | return await this.bot.receiveInternal(this._sanitize(m)); 38 | } 39 | } 40 | async pass(m) { 41 | this.respond(await this.receive(m)); 42 | } 43 | 44 | error(s) { 45 | this.respond(this.new().error(s).build()); 46 | } 47 | 48 | copy(m) { 49 | return new MessageBuilder().raw(m); 50 | } 51 | } -------------------------------------------------------------------------------- /src/super-bot/index.js: -------------------------------------------------------------------------------- 1 | export { default as SuperBot } from './SuperBot' 2 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | require = require("esm")(module /*, options*/); 6 | module.exports = require("./src/app.js"); -------------------------------------------------------------------------------- /test/plugins/plugin.echo.test.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); 2 | 3 | describe('plugin.echo', function() { 4 | it('should return same text', async function() { 5 | let response = await this.bot.receive(this.builder.text('echo Test').build()); 6 | response.text.should.be.equal('Test'); 7 | }); 8 | }); -------------------------------------------------------------------------------- /test/setup.test.js: -------------------------------------------------------------------------------- 1 | import { SuperBot } from "../src/super-bot"; 2 | import MessageBuilder from "../src/super-bot/MessageBuilder"; 3 | 4 | before(async function() { 5 | this.timeout(6000); 6 | 7 | const config = require('config'); 8 | this.bot = new SuperBot(config.get("SuperBot")); 9 | return await this.bot.start(); 10 | }); 11 | 12 | beforeEach(function() { 13 | this.builder = new MessageBuilder(); 14 | this.builder.raw({ 15 | sender: { id: '123456', isMe: true }, 16 | chat: { id: '654321' } 17 | }); 18 | }); --------------------------------------------------------------------------------