├── .gitattributes ├── .gitignore ├── README.md ├── config-example.json ├── index.js ├── package-lock.json └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | config.json 4 | .lastcheck -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord RSS bot 2 | 3 | This is a simple Discord bot that can parse RSS feeds and send updates to specified text channels. 4 | You can add multiple feeds. 5 | 6 | ## Installation 7 | `npm install` 8 | 9 | Rename `config-example.json` to `config.json` and fill in the correct values. 10 | 11 | ## Usage 12 | `node index.js` 13 | 14 | ## Author 15 | :shipit: Lev Angel 16 | -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "DISCORD_BOT_TOKEN", 3 | "checkInterval": 5, 4 | "botActivity": "Google Reader", 5 | 6 | "channels": { 7 | "forum" : { 8 | "feed" : "https://site.com/index.rss", 9 | "channelId": "DISCORD_CHANNEL_ID", 10 | "type": "New message in RSS" 11 | }, 12 | "youtube": { 13 | "feed" : "https://www.youtube.com/feeds/videos.xml?channel_id=YOUTUBE_CHANNEL_ID", 14 | "channelId": "DISCORD_CHANNEL_ID", 15 | "type": "New video" 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const Parser = require('rss-parser'); 3 | const fs = require('fs').promises; 4 | 5 | const client = new Discord.Client(); 6 | const rss = new Parser(); 7 | const config = require('./config.json'); 8 | 9 | client.on('ready', () => { 10 | console.log('Client ready!'); 11 | client.user.setActivity(config.botActivity); 12 | checkRssFeeds(); 13 | setInterval(checkRssFeeds, config.checkInterval * 60 * 1000); 14 | }); 15 | 16 | client.login(config.token); 17 | 18 | 19 | async function checkRssFeeds(){ 20 | const lastCheck = await getLastCheckTimestamp(); 21 | 22 | for (const channel in config.channels){ 23 | await parseChannel(channel, lastCheck); 24 | } 25 | 26 | await setLastCheckTimestamp(); 27 | } 28 | 29 | async function parseChannel(channel, lastCheck){ 30 | const { feed, channelId, type } = config.channels[channel]; 31 | const discordChannel = client.channels.cache.get(channelId); 32 | const rssFeed = await rss.parseURL(feed); 33 | 34 | rssFeed.items.reverse().forEach(item => { 35 | const { title, link, isoDate} = item; 36 | if(lastCheck > getRecordPubTimestamp( isoDate )) return; 37 | 38 | sendEmbed(discordChannel, title, link, type); 39 | }); 40 | } 41 | 42 | function sendEmbed(channel, title, description, footer){ 43 | const embed = new Discord.MessageEmbed() 44 | .setTitle(title) 45 | .setColor(0x7d7d7d) 46 | .setDescription(description) 47 | .setFooter(footer); 48 | channel.send(embed); 49 | } 50 | 51 | async function getLastCheckTimestamp(){ 52 | try { 53 | return await fs.readFile( __dirname + '/.lastcheck', 'utf8'); 54 | } catch(e) { 55 | return Date.now(); 56 | } 57 | } 58 | 59 | async function setLastCheckTimestamp(){ 60 | try { 61 | return await fs.writeFile( __dirname + '/.lastcheck', Date.now().toString()); 62 | } catch(e) { 63 | console.log(e); 64 | } 65 | } 66 | 67 | // Convert date 2020-05-23T10:00:16.000Z to timestamp in milliseconds 68 | function getRecordPubTimestamp(isoDate){ 69 | return Date.parse( isoDate ); 70 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rage-script-rss-bot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@discordjs/collection": { 8 | "version": "0.1.6", 9 | "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", 10 | "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" 11 | }, 12 | "@discordjs/form-data": { 13 | "version": "3.0.1", 14 | "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", 15 | "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", 16 | "requires": { 17 | "asynckit": "^0.4.0", 18 | "combined-stream": "^1.0.8", 19 | "mime-types": "^2.1.12" 20 | } 21 | }, 22 | "abort-controller": { 23 | "version": "3.0.0", 24 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 25 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 26 | "requires": { 27 | "event-target-shim": "^5.0.0" 28 | } 29 | }, 30 | "asynckit": { 31 | "version": "0.4.0", 32 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 33 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 34 | }, 35 | "combined-stream": { 36 | "version": "1.0.8", 37 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 38 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 39 | "requires": { 40 | "delayed-stream": "~1.0.0" 41 | } 42 | }, 43 | "delayed-stream": { 44 | "version": "1.0.0", 45 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 46 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 47 | }, 48 | "discord.js": { 49 | "version": "12.5.3", 50 | "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", 51 | "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", 52 | "requires": { 53 | "@discordjs/collection": "^0.1.6", 54 | "@discordjs/form-data": "^3.0.1", 55 | "abort-controller": "^3.0.0", 56 | "node-fetch": "^2.6.1", 57 | "prism-media": "^1.2.9", 58 | "setimmediate": "^1.0.5", 59 | "tweetnacl": "^1.0.3", 60 | "ws": "^7.4.4" 61 | } 62 | }, 63 | "entities": { 64 | "version": "2.2.0", 65 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 66 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" 67 | }, 68 | "event-target-shim": { 69 | "version": "5.0.1", 70 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 71 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" 72 | }, 73 | "mime-db": { 74 | "version": "1.47.0", 75 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", 76 | "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" 77 | }, 78 | "mime-types": { 79 | "version": "2.1.30", 80 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", 81 | "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", 82 | "requires": { 83 | "mime-db": "1.47.0" 84 | } 85 | }, 86 | "node-fetch": { 87 | "version": "2.6.1", 88 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 89 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 90 | }, 91 | "prism-media": { 92 | "version": "1.2.9", 93 | "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", 94 | "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==" 95 | }, 96 | "rss-parser": { 97 | "version": "3.12.0", 98 | "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.12.0.tgz", 99 | "integrity": "sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A==", 100 | "requires": { 101 | "entities": "^2.0.3", 102 | "xml2js": "^0.4.19" 103 | } 104 | }, 105 | "sax": { 106 | "version": "1.2.4", 107 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 108 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 109 | }, 110 | "setimmediate": { 111 | "version": "1.0.5", 112 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 113 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" 114 | }, 115 | "tweetnacl": { 116 | "version": "1.0.3", 117 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", 118 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" 119 | }, 120 | "ws": { 121 | "version": "7.4.5", 122 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", 123 | "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" 124 | }, 125 | "xml2js": { 126 | "version": "0.4.23", 127 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", 128 | "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", 129 | "requires": { 130 | "sax": ">=0.6.0", 131 | "xmlbuilder": "~11.0.0" 132 | } 133 | }, 134 | "xmlbuilder": { 135 | "version": "11.0.1", 136 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 137 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rage-script-rss-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "discord.js": "^12.5.3", 14 | "rss-parser": "^3.12.0" 15 | } 16 | } 17 | --------------------------------------------------------------------------------