├── .prettierrc ├── .gitignore ├── Procfile ├── .babelrc ├── README.md ├── package.json ├── LICENSE ├── src ├── index.js └── searchBooks.js └── dist ├── index.js ├── app.js └── searchBooks.js /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: node dist/index.js -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-runtime", 6 | { 7 | "regenerator": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BooksAndBot 2 | 3 | [@BooksAndBot](https://telegram.me/BooksAndBot) is an inline bot that allows you to search for books and share them in a conversation. 4 | 5 | Powered by [Goodreads](https://www.goodreads.com/). 6 | 7 | ## Demo 8 | 9 | ![How it works](https://media.giphy.com/media/ekYwbZFeSVk7jsDFKZ/giphy.gif) 10 | 11 | ## Development 12 | 13 | 1. You'll need a few things: 14 | 15 | - A Telegram bot token. Contact [@BotFather](http://telegram.me/BotFather) in order to create a new bot and receive a token. 16 | - A Goodreads API key. Apply for one [here](https://www.goodreads.com/api). 17 | 18 | 2. Create `.env` file in the root directory with following variables: 19 | 20 | ```shell 21 | BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN 22 | GOODREADS_API_KEY=YOUR_GOODREADS_API_KEY 23 | ``` 24 | 25 | 3. Install dependencies and start the app: 26 | 27 | ```shell 28 | npm i 29 | npm start 30 | ``` 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "books-and-bot", 3 | "version": "1.0.0", 4 | "description": "Telegram bot", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "nodemon --exec babel-node src/index.js", 8 | "build": "babel src --out-dir dist", 9 | "serve": "node dist/index.js" 10 | }, 11 | "author": "dmtrbrl (https://github.com/dmtrbrl)", 12 | "license": "ISC", 13 | "dependencies": { 14 | "babel-polyfill": "^6.26.0", 15 | "dotenv": "^8.2.0", 16 | "node-fetch": "^2.6.0", 17 | "telegraf": "^3.34.1", 18 | "xml2js-es6-promise": "^1.1.1" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.7.4", 22 | "@babel/core": "^7.7.4", 23 | "@babel/node": "^7.7.4", 24 | "@babel/plugin-transform-runtime": "^7.7.4", 25 | "@babel/preset-env": "^7.7.4", 26 | "@babel/runtime": "^7.7.4", 27 | "nodemon": "^1.19.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dmytro Barylo 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 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { } from "dotenv/config"; 2 | import Telegraf from "telegraf"; 3 | import searchBooks from "./searchBooks"; 4 | 5 | const BOT_TOKEN = process.env.BOT_TOKEN || ""; 6 | const PORT = process.env.PORT || 3000; 7 | const URL = process.env.URL || "https://books-and-bot.herokuapp.com/"; 8 | 9 | const createMessageText = book => { 10 | return ` 11 | ${book.title} 12 | ${book.author} 13 | `; 14 | }; 15 | 16 | const bot = new Telegraf(BOT_TOKEN); 17 | bot.on("inline_query", async ctx => { 18 | const searchResults = await searchBooks(ctx.inlineQuery.query); 19 | const results = 20 | searchResults && searchResults.length 21 | ? searchResults.map((book, id) => ({ 22 | id, 23 | type: "article", 24 | title: book.title, 25 | description: book.author, 26 | thumb_url: book.thumb_url, 27 | input_message_content: { 28 | message_text: createMessageText(book), 29 | parse_mode: "HTML" 30 | }, 31 | reply_markup: { 32 | inline_keyboard: [ 33 | [ 34 | { 35 | text: "Show on Goodreads", 36 | url: book.url 37 | } 38 | ] 39 | ] 40 | } 41 | })) 42 | : []; 43 | ctx.answerInlineQuery(results); 44 | }); 45 | bot.telegram.setWebhook(`${URL}/bot${BOT_TOKEN}`); 46 | bot.startWebhook(`/bot${BOT_TOKEN}`, null, PORT); 47 | bot.launch(); 48 | -------------------------------------------------------------------------------- /src/searchBooks.js: -------------------------------------------------------------------------------- 1 | import {} from "dotenv/config"; 2 | import fetch from "node-fetch"; 3 | import querystring from "querystring"; 4 | import xml2js from "xml2js-es6-promise"; 5 | 6 | export default async query => { 7 | if (!query) return null; 8 | query = querystring.escape(query); 9 | let data = null; 10 | const url = `https://www.goodreads.com/search/index.xml?key=${ 11 | process.env.GOODREADS_API_KEY 12 | }&q=${query}&field=title`; 13 | 14 | try { 15 | const response = await fetch(url); 16 | const text = await response.text(); 17 | const js = await xml2js(text); 18 | 19 | if (js.GoodreadsResponse) 20 | data = js.GoodreadsResponse.search[0].results[0].work.map(result => { 21 | const book = result.best_book[0]; 22 | let title = book.title[0]; 23 | let author = `by ${book.author.map(a => a.name).join(", ")}`; 24 | let url = `https://www.goodreads.com/book/show/${book.id[0]._}`; 25 | let thumb_url = book.image_url[0]; 26 | let cover_url = null; 27 | 28 | // Using dirty and unstable way to get a book cover :( 29 | if (thumb_url.indexOf("._SX") !== -1) { 30 | let n = thumb_url.lastIndexOf("._SX"); 31 | cover_url = `${thumb_url.slice(0, n)}._SY400_.jpg`; 32 | } else if (thumb_url.indexOf("._SY") !== -1) { 33 | let n = thumb_url.lastIndexOf("._SY"); 34 | cover_url = `${thumb_url.slice(0, n)}._SY400_.jpg`; 35 | } else { 36 | cover_url = thumb_url; 37 | } 38 | 39 | return { 40 | title, 41 | author, 42 | url, 43 | thumb_url, 44 | cover_url 45 | }; 46 | }); 47 | } catch (error) { 48 | console.log(error); 49 | } 50 | 51 | return data; 52 | }; 53 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 6 | 7 | require("dotenv/config"); 8 | 9 | var _telegraf = _interopRequireDefault(require("telegraf")); 10 | 11 | var _searchBooks = _interopRequireDefault(require("./searchBooks")); 12 | 13 | var BOT_TOKEN = process.env.BOT_TOKEN || ""; 14 | var PORT = process.env.PORT || 3000; 15 | var URL = process.env.URL || "https://books-and-bot.herokuapp.com/"; 16 | 17 | var createMessageText = function createMessageText(book) { 18 | return "\n".concat(book.title, "\n").concat(book.author, "\n"); 19 | }; 20 | 21 | var bot = new _telegraf["default"](BOT_TOKEN); 22 | bot.on("inline_query", function _callee(ctx) { 23 | var searchResults, results; 24 | return _regenerator["default"].async(function _callee$(_context) { 25 | while (1) { 26 | switch (_context.prev = _context.next) { 27 | case 0: 28 | _context.next = 2; 29 | return _regenerator["default"].awrap((0, _searchBooks["default"])(ctx.inlineQuery.query)); 30 | 31 | case 2: 32 | searchResults = _context.sent; 33 | results = searchResults && searchResults.length ? searchResults.map(function (book, id) { 34 | return { 35 | id: id, 36 | type: "article", 37 | title: book.title, 38 | description: book.author, 39 | thumb_url: book.thumb_url, 40 | input_message_content: { 41 | message_text: createMessageText(book), 42 | parse_mode: "HTML" 43 | }, 44 | reply_markup: { 45 | inline_keyboard: [[{ 46 | text: "Show on Goodreads", 47 | url: book.url 48 | }]] 49 | } 50 | }; 51 | }) : []; 52 | ctx.answerInlineQuery(results); 53 | 54 | case 5: 55 | case "end": 56 | return _context.stop(); 57 | } 58 | } 59 | }); 60 | }); 61 | bot.telegram.setWebhook("".concat(URL, "/bot").concat(BOT_TOKEN)); 62 | bot.startWebhook("/bot".concat(BOT_TOKEN), null, PORT); 63 | bot.launch(); -------------------------------------------------------------------------------- /dist/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 6 | 7 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 8 | 9 | require("dotenv/config"); 10 | 11 | var _telegraf = _interopRequireDefault(require("telegraf")); 12 | 13 | var _searchBooks = _interopRequireDefault(require("./searchBooks")); 14 | 15 | var createMessageText = function createMessageText(book) { 16 | return "\n".concat(book.title, "\n").concat(book.author, "\n"); 17 | }; 18 | 19 | var bot = new _telegraf["default"](process.env.BOT_TOKEN); 20 | bot.on("inline_query", 21 | /*#__PURE__*/ 22 | function () { 23 | var _ref = (0, _asyncToGenerator2["default"])( 24 | /*#__PURE__*/ 25 | _regenerator["default"].mark(function _callee(ctx) { 26 | var searchResults, results; 27 | return _regenerator["default"].wrap(function _callee$(_context) { 28 | while (1) { 29 | switch (_context.prev = _context.next) { 30 | case 0: 31 | _context.next = 2; 32 | return (0, _searchBooks["default"])(ctx.inlineQuery.query); 33 | 34 | case 2: 35 | searchResults = _context.sent; 36 | results = searchResults && searchResults.length ? searchResults.map(function (book, id) { 37 | return { 38 | id: id, 39 | type: "article", 40 | title: book.title, 41 | description: book.author, 42 | thumb_url: book.thumb_url, 43 | input_message_content: { 44 | message_text: createMessageText(book), 45 | parse_mode: "HTML" 46 | }, 47 | reply_markup: { 48 | inline_keyboard: [[{ 49 | text: "Show on Goodreads", 50 | url: book.url 51 | }]] 52 | } 53 | }; 54 | }) : []; 55 | ctx.answerInlineQuery(results); 56 | 57 | case 5: 58 | case "end": 59 | return _context.stop(); 60 | } 61 | } 62 | }, _callee); 63 | })); 64 | 65 | return function (_x) { 66 | return _ref.apply(this, arguments); 67 | }; 68 | }()); 69 | bot.launch(); -------------------------------------------------------------------------------- /dist/searchBooks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 11 | 12 | require("dotenv/config"); 13 | 14 | var _nodeFetch = _interopRequireDefault(require("node-fetch")); 15 | 16 | var _querystring = _interopRequireDefault(require("querystring")); 17 | 18 | var _xml2jsEs6Promise = _interopRequireDefault(require("xml2js-es6-promise")); 19 | 20 | var _callee = function _callee(query) { 21 | var data, url, response, text, js; 22 | return _regenerator["default"].async(function _callee$(_context) { 23 | while (1) { 24 | switch (_context.prev = _context.next) { 25 | case 0: 26 | if (query) { 27 | _context.next = 2; 28 | break; 29 | } 30 | 31 | return _context.abrupt("return", null); 32 | 33 | case 2: 34 | query = _querystring["default"].escape(query); 35 | data = null; 36 | url = "https://www.goodreads.com/search/index.xml?key=".concat(process.env.GOODREADS_API_KEY, "&q=").concat(query, "&field=title"); 37 | _context.prev = 5; 38 | _context.next = 8; 39 | return _regenerator["default"].awrap((0, _nodeFetch["default"])(url)); 40 | 41 | case 8: 42 | response = _context.sent; 43 | _context.next = 11; 44 | return _regenerator["default"].awrap(response.text()); 45 | 46 | case 11: 47 | text = _context.sent; 48 | _context.next = 14; 49 | return _regenerator["default"].awrap((0, _xml2jsEs6Promise["default"])(text)); 50 | 51 | case 14: 52 | js = _context.sent; 53 | if (js.GoodreadsResponse) data = js.GoodreadsResponse.search[0].results[0].work.map(function (result) { 54 | var book = result.best_book[0]; 55 | var title = book.title[0]; 56 | var author = "by ".concat(book.author.map(function (a) { 57 | return a.name; 58 | }).join(", ")); 59 | var url = "https://www.goodreads.com/book/show/".concat(book.id[0]._); 60 | var thumb_url = book.image_url[0]; 61 | var cover_url = null; // Using dirty and unstable way to get a book cover :( 62 | 63 | if (thumb_url.indexOf("._SX") !== -1) { 64 | var n = thumb_url.lastIndexOf("._SX"); 65 | cover_url = "".concat(thumb_url.slice(0, n), "._SY400_.jpg"); 66 | } else if (thumb_url.indexOf("._SY") !== -1) { 67 | var _n = thumb_url.lastIndexOf("._SY"); 68 | 69 | cover_url = "".concat(thumb_url.slice(0, _n), "._SY400_.jpg"); 70 | } else { 71 | cover_url = thumb_url; 72 | } 73 | 74 | return { 75 | title: title, 76 | author: author, 77 | url: url, 78 | thumb_url: thumb_url, 79 | cover_url: cover_url 80 | }; 81 | }); 82 | _context.next = 21; 83 | break; 84 | 85 | case 18: 86 | _context.prev = 18; 87 | _context.t0 = _context["catch"](5); 88 | console.log(_context.t0); 89 | 90 | case 21: 91 | return _context.abrupt("return", data); 92 | 93 | case 22: 94 | case "end": 95 | return _context.stop(); 96 | } 97 | } 98 | }, null, null, [[5, 18]]); 99 | }; 100 | 101 | exports["default"] = _callee; --------------------------------------------------------------------------------