├── .gitignore ├── CONTRIBUTING.md ├── Procfile ├── README.md ├── astroBotDiscord.js ├── astroBotSlack.js ├── config.json ├── license.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Hide npm cache 10 | .npm 11 | 12 | # Hide REPL history used for testing 13 | .node_repl_history 14 | 15 | # Hide files with highly sensitive information 16 | config.json 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering contributing and making our planet easier to explore! 4 | 5 | We're excited you would like to contribute to Astrobot! Whether you're finding bugs, adding new features, fixing anything broken, or improving documentation, get started by submitting an issue or pull request! 6 | 7 | ## Submitting an Issue 8 | 9 | If you have any questions or ideas, or notice any problems or bugs, first [search open issues](https://github.com/nasa/astrobot/issues) to see if the issue has already been submitted. We may already be working on the issue. If you think your issue is new, you're welcome to [create a new issue](https://github.com/nasa/astrobot/issues/new). 10 | 11 | ## Pull Requests 12 | 13 | If you want to submit your own contributions, follow these steps: 14 | 15 | * Fork the astrobot repo 16 | * Create a new branch from the branch you'd like to contribute to 17 | * If an issue does't already exist, submit one (see above) 18 | * [Create a pull request](https://help.github.com/articles/creating-a-pull-request/) from your fork into the target branch of the nasa/astrobot repo 19 | * Be sure to [mention the corresponding issue number](https://help.github.com/articles/closing-issues-using-keywords/) in the PR description, i.e. "Fixes Issue #10" 20 | * Upon submission of a pull request, the Astrobot development team will review the code 21 | * The request will then either be merged, declined, or an adjustment to the code will be requested 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node astroBot.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astrobot 2 | A Slack/Discord bot integration with NASA data. 3 | 4 | This is a Slack/Discord bot that is designed to use the NASA APOD API to allow users to query the API through Slack or Discord. 5 | 6 | ## Contributing 7 | We do accept pull requests from the public. Please note that we can be slow to respond. Please be patient. Pull requests should not impact previous functionality. 8 | 9 | ## Feedback 10 | Star this repo if you found it useful. Use the github issue tracker to give feedback on this repo. 11 | -------------------------------------------------------------------------------- /astroBotDiscord.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const config = require('./config'); 3 | const superagent = require('superagent'); 4 | const stripIndent = require('strip-indent'); 5 | 6 | const NASA = new Discord.Client(); 7 | 8 | NASA.on('ready', () => { 9 | console.log('Logged into Discord as ' + NASA.user.username); 10 | console.log('You are in ' + NASA.guilds.size + ` guild${NASA.guilds.size == 1? '' : 's'}!`); 11 | NASA.prefix = new RegExp('^<@!?' + NASA.user.id + '>'); 12 | }); 13 | 14 | const commands = { 15 | 'help': (message) => sendHelp(message), 16 | 'apod': (message, argument, dm) => { 17 | let date; 18 | switch (argument) { 19 | case 'random': 20 | const firstAPODDate = new Date(96, 6, 16); 21 | const currentDate = new Date(); 22 | date = new Date(Math.floor(firstAPODDate.getTime() + Math.random() * (currentDate.getTime() - firstAPODDate.getTime()))); 23 | break; 24 | case 'today': 25 | date = new Date(); 26 | break; 27 | case 'yesterday': 28 | date = ((d) => new Date(d.setDate(d.getDate() - 1)))(new Date()); 29 | break; 30 | default: 31 | date = argument == undefined ? new Date() : new Date(argument); 32 | if (date == 'Invalid Date') { 33 | return message.channel.send('That date was invalid, try something like `Janary 30, 1995`, or `1995-1-30`'); 34 | } 35 | break; 36 | } 37 | getAPODImage(date).then((data) => { 38 | if (!dm) { 39 | message.channel.startTyping(); 40 | message.channel.send('', new Discord.Attachment(data.url, 'APOD.jpg')).then(() => message.channel.stopTyping()); 41 | } 42 | if (dm) { 43 | message.author.send('', new Discord.Attachment(data.url, 'APOD.jpg')); 44 | } 45 | }); 46 | }, 47 | }; 48 | 49 | NASA.on('message', (message) => { 50 | if (!NASA.prefix.test(message.content)) return; 51 | 52 | // [0]: Command, [1] Argument 53 | message.content = message.content.replace(/[ ]{2,}/, ' '); 54 | const dm = message.content.includes('-dm'); 55 | const split = message.content.split(' '); 56 | if (dm) split.splice(split.length - 1, 1); 57 | const command = split[1]; 58 | let argument; 59 | if (split.length == 3) argument = split[2]; 60 | if (split.length > 3) argument = `${split[2]} ${parseInt(split[3].replace(/[a-z]/gi, ''))-1} ${split[4]}`; 61 | 62 | if (!command) return; 63 | switch (command.toLowerCase()) { 64 | case 'hi': 65 | case 'hey': 66 | case 'hello': 67 | message.reply('Hi!'); 68 | return; 69 | default: 70 | if (command.toLowerCase() in commands) { 71 | commands[command.toLowerCase()](message, argument, dm); 72 | return; 73 | } 74 | } 75 | }); 76 | 77 | function sendHelp(message) { 78 | if (message.channel.type != 'dm') message.channel.send('Sending you a DM...').then((message) => message.delete(3000)); 79 | message.author.send(stripIndent(` 80 | Here's some information on how I can be used. 81 | 82 | apod: Display an Astronomy Picture of the Day image. 83 | usage: \`<@${NASA.user.id}> apod {option}\` 84 | **Options:** 85 | End your command with \`-dm\` to recieve as a DM, rather than the channel. 86 | random: display a random day's APOD image. 87 | today: display today's APOD image. 88 | yesterday: display yesterday's APOD image. 89 | {date} displays a specific date's APOD image with {date} in the format: YYYY-MM-DD (Ex: 2016-10-24) 90 | `)); 91 | } 92 | 93 | function getAPODImage(date) { 94 | return new Promise((resolve, reject) => { 95 | date = formatDate(date); 96 | superagent.get(`https://api.nasa.gov/planetary/apod?concept_tags=false&api_key=${config.nasa.apiKey}&date=${date}`) 97 | .end((err, res) => { 98 | if (err) return reject(err); 99 | return resolve(res.body); 100 | }); 101 | }); 102 | } 103 | 104 | 105 | function formatDate(date) { 106 | let d = new Date(date); 107 | let month = '' + (d.getMonth() + 1); 108 | let day = '' + d.getDate(); 109 | let year = d.getFullYear(); 110 | if (month.length < 2) month = '0' + month; 111 | if (day.length < 2) day = '0' + day; 112 | return [year, month, day].join('-'); 113 | } 114 | 115 | NASA.login(config.discord.token); 116 | -------------------------------------------------------------------------------- /astroBotSlack.js: -------------------------------------------------------------------------------- 1 | var Slack = require('slack-client'); 2 | var request = require('request'); 3 | var express = require('express'); 4 | var http = require('http'); 5 | var https = require('https'); 6 | var config = require('./config'); 7 | setInterval(function () { 8 | if (config.heroku.url.substring(0, 5) === 'https') { 9 | https.get(config.heroku.url); 10 | } else { 11 | http.get(config.heroku.url); 12 | } 13 | }, (config.heroku.checkInterval * 60) * 1000); // every 5 minutes(300000) 14 | 15 | // Unique token for the AstroBot 16 | var token = config.slack.token; 17 | 18 | // Setup an instance of slack with the above token 19 | var slack = new Slack(token, true, true); 20 | var app = express(); 21 | var port = process.env.PORT || 3000; 22 | 23 | app.listen(port, function () { 24 | console.log('Slack bot listening on port ' + port); 25 | }); 26 | 27 | // Commands to execute when the bot is opened 28 | slack.on('open', function () { 29 | var channels = Object.keys(slack.channels) 30 | .map(function (k) { 31 | return slack.channels[k]; 32 | }) 33 | .filter(function (c) { 34 | return c.is_member; 35 | }) 36 | .map(function (c) { 37 | return c.name; 38 | }); 39 | var groups = Object.keys(slack.groups) 40 | .map(function (k) { 41 | return slack.groups[k]; 42 | }) 43 | .filter(function (g) { 44 | return g.is_open && !g.is_archived; 45 | }) 46 | .map(function (g) { 47 | return g.name; 48 | }); 49 | console.log('Welcome to Slack. You are ' + slack.self.name + ' of ' + slack.team.name); 50 | if (channels.length > 0) { 51 | console.log('You are in: ' + channels.join(', ')); 52 | } else { 53 | console.log('You are not in any channels.'); 54 | } 55 | if (groups.length > 0) { 56 | console.log('As well as: ' + groups.join(', ')); 57 | } 58 | }); 59 | 60 | // Commands to execute when a message is received 61 | slack.on('message', function (message) { 62 | var channel = slack.getChannelGroupOrDMByID(message.channel); 63 | var user = slack.getUserByID(message.user); 64 | // Checks if the message is a message (not an edited, etc) and that the message is to AstroBot 65 | if (message.type === 'message' && isDirect(slack.self.id, message.text)) { 66 | // [0]: Username, [1]: Command, [2]: Options, [3] Arguements 67 | var messageArray = message.text.split(' '); 68 | // The mention of the bot (to be discarded) 69 | var username = messageArray[0]; // eslint-disable-line 70 | // APOD or Help 71 | var command = messageArray[1]; 72 | // Me, Us, #Channel, APOD 73 | var option = messageArray[2]; 74 | // Today, Yesterday, Random, YYYY-mm-DD 75 | var arguement = messageArray[3]; 76 | // Check for specified command (apod, help) 77 | if (command != null) { 78 | switch (command.toUpperCase()) { 79 | case 'apod'.toUpperCase(): 80 | ProcessOptions(option, arguement, channel, user) 81 | break; 82 | case 'help'.toUpperCase(): 83 | ProcessOptions(option, arguement, channel, user); 84 | break; 85 | case 'hi'.toUpperCase(): 86 | case 'hey'.toUpperCase(): 87 | case 'hello'.toUpperCase(): 88 | channel.send('Hi!'); 89 | break; 90 | default: 91 | channel.send('Invalid command. Please use \'@astrobot help\' for more information.'); 92 | break 93 | } 94 | } else { 95 | // Send a brief help message if there was no command 96 | channel.send('Type \'@astrobot: help\' for usage.'); 97 | } 98 | } 99 | }); 100 | // Process the options that the user specified 101 | function ProcessOptions (option, arguement, channel, user) { 102 | // Check for specified options 103 | if (option != null) { 104 | // Check for the 'me' option (DM to user) 105 | if (option.toUpperCase() === 'me'.toUpperCase()) { 106 | // ProcessArguements(arguement, user); 107 | slack.openDM(user.id, function (res) { 108 | channel = slack.getChannelGroupOrDMByID(res.channel.id); 109 | ProcessArguements(arguement, channel, user); 110 | }); 111 | } 112 | // Check for the 'us' option (send to the channel the request was maade in) 113 | else if (option.toUpperCase() === 'us'.toUpperCase()) { 114 | // Process arguements for the current channel 115 | ProcessArguements(arguement, channel, user); 116 | } 117 | // Check for the 'channel' option (send to the specified channel) 118 | else if (option.toUpperCase().indexOf('#'.toUpperCase()) > -1) { 119 | // Option format is <@CHANNEL NAME> - substring to remove <@> 120 | option = option.substring(2, option.length - 1); 121 | // Get the channel by the ID of the option 122 | channel = slack.getChannelByID(option); 123 | var message = 'Image requested by @' + user.name; 124 | // Process the arguements for the given channel 125 | ProcessArguements(arguement, channel, user, message); 126 | } 127 | // This should follow 128 | else if (option.toUpperCase() === 'apod'.toUpperCase()) { 129 | ProcessHelp(channel, user); 130 | } else if (option.toUpperCase() === 'help'.toUpperCase()) { 131 | channel.send('help (?,h): Describe the usage of this bot or its subcommands.\n' + 132 | 'usage: help [SUBCOMMAND...] (Ex: @astrobot help apod)'); 133 | } else { 134 | try { 135 | message = 'Image requested by @' + user.name; 136 | channel.send(message + '\nInvalid option. Please use \'@astrobot help\' for more information.'); 137 | } catch (err) { 138 | slack.openDM(user.id, function (res) { 139 | var dmChannel = slack.getChannelGroupOrDMByID(res.channel.id); 140 | dmChannel.send('Invalid option. Please use \'@astrobot help\' for more information.'); // ProcessHelp(dmChannel); 141 | }); 142 | } 143 | } 144 | } 145 | // There are no options, show help 146 | else { 147 | ProcessHelpNoDetails(channel, user); 148 | } 149 | } 150 | 151 | // Process the command arguements and display the image 152 | function ProcessArguements (arguement, channel, user, message) { 153 | if (arguement != null) { 154 | // If there is no message (undefined), create a blank message 155 | if (message === undefined) { 156 | message = ''; 157 | } 158 | // APOD Today 159 | if (arguement.toUpperCase() === 'today'.toUpperCase()) { 160 | loadNewImage(new Date(), channel, user, message); 161 | } 162 | // APOD Yesterday 163 | else if (arguement.toUpperCase() === 'yesterday'.toUpperCase()) { 164 | var yesterdaysDate = new Date(); 165 | yesterdaysDate = yesterdaysDate.setDate(yesterdaysDate.getDate() - 1); 166 | loadNewImage(yesterdaysDate, channel, user, message); 167 | } 168 | // APOD random 169 | else if (arguement.toUpperCase() === 'random'.toUpperCase()) { 170 | var numberOfDays = CalculateDaysFromFirstImage(); 171 | var randomInt = randomIntFromInterval(1, numberOfDays); 172 | var randomDate = new Date(); 173 | randomDate = randomDate.setDate(randomDate.getDate() - randomInt); 174 | loadNewImage(randomDate, channel, user, message); 175 | } 176 | // APOD Date 177 | else if (isValidDate(arguement)) { 178 | loadNewImage(arguement, channel, user, message); 179 | } 180 | // Not a valid date, DM user help 181 | else if (!isValidDate(arguement)) { 182 | try { 183 | channel.send(message + '\nInvalid arguement. Please use \'@astrobot help\' for more information.'); 184 | } catch (err) { 185 | slack.openDM(user.id, function (res) { 186 | var dmChannel = slack.getChannelGroupOrDMByID(res.channel.id); 187 | dmChannel.send('Invalid arguement. Please use \'@astrobot help\' for more information.'); // ProcessHelp(dmChannel); 188 | }); 189 | } 190 | } 191 | } else { 192 | ProcessHelp(channel, user); 193 | } 194 | } 195 | // Query the URL for a given date 196 | function loadNewImage (date, channel, user, message) { 197 | var formattedDate = formatDate(date); 198 | message += '\n APOD Image for ' + formattedDate; 199 | request({ 200 | url: 'https://api.nasa.gov/planetary/apod?concept_tags=false&api_key=' + config.nasa.apiKey + '&date=' + formattedDate, 201 | json: true 202 | }, function (error, response, body) { 203 | if (!error && response.statusCode === 200) { 204 | SendToChannelCB(body.url, body.title, body.explanation, formattedDate, channel, user, message); 205 | } 206 | }); 207 | } 208 | // Callback function after the JSON has been parsed and we have the URL 209 | function SendToChannelCB (url, title, description, date, channel, user, message) { 210 | try { 211 | channel.postMessage({ 212 | 'attachments': [{ 213 | 'text': message + '\n' + title + '\n' + description, 214 | 'image_url': url, 215 | 'color': '#FFFFFF', 216 | 'mrkdwn_in': ['text'] 217 | }], 218 | 'username': 'AstroBot', 219 | 'icon_url': 'http://i.imgur.com/1ovnJeD.png', 220 | 'unfurl_links': false, 221 | 'unfurl_media': true 222 | }); 223 | } catch (err) { 224 | slack.openDM(user.id, function (res) { 225 | var dmChannel = slack.getChannelGroupOrDMByID(res.channel.id); 226 | dmChannel.send('Invalid channel. Please use \'@astrobot help\' for more information.'); // ProcessHelp(dmChannel); 227 | }); 228 | } 229 | } 230 | // Send help message to the specified channel 231 | function ProcessHelp (channel, user) { 232 | channel.send('Help requested by: @' + user.name + '\n\n' + 233 | 'apod : Display an Astronomy Picture of the Day image.\n' + 234 | 'usage : @astrobot apod {option} {arguement}\n\n' + 235 | 'Valid options:\n' + 236 | ' me \t\t \t \t \t \t \t: display the image in a direct message to me.\n' + 237 | ' us \t \t \t \t \t \t \t: display the image in the current channel or group.\n' + 238 | ' #channel_name \t: display the image in a specified channel.\n\n' + 239 | 'Valid arguements:\n' + 240 | ' random \t \t \t: display a random day\'s APOD image.\n' + 241 | ' today \t \t \t \t: display today\'s APOD image.\n' + 242 | ' yesterday \t \t: display yesterday\'s APOD image.\n' + 243 | ' {date} \t \t \t \t: displays a specific date\'s APOD image with {date} in the format: MM-DD-YYYY (Ex: 05/25/2006)'); 244 | } 245 | 246 | function ProcessHelpNoDetails (channel, user) { 247 | channel.send('Help requested by: @' + user.name + '\n\n' + 248 | 'Use @astrobot help apod for details about each option and arguement\n\n' + 249 | 'apod : Display an Astronomy Picture of the Day image.\n' + 250 | 'usage : @astrobot apod {option} {arguement}\n\n' + 251 | 'Valid options:\n' + 252 | ' me\n' + 253 | ' us\n' + 254 | ' #channel_name\n\n' + 255 | 'Valid arguements:\n' + 256 | ' random\n' + 257 | ' today\n' + 258 | ' yesterday\n' + 259 | ' {date}'); 260 | } 261 | // Check if a given date is valid 262 | function isValidDate (str) { 263 | var matches = str.match(/(\d{1,2})[- . \/](\d{1,2})[- . \/](\d{4})$/); 264 | if (!matches) return; 265 | // parse each piece and see if it makes a valid date object 266 | var month = parseInt(matches[1], 10); 267 | var day = parseInt(matches[2], 10); 268 | var year = parseInt(matches[3], 10); 269 | var date = new Date(year, month - 1, day); 270 | if (!date || !date.getTime()) return; 271 | // make sure we have no funny rollovers that the date object sometimes accepts 272 | // month > 12, day > what's allowed for the month 273 | if (date.getMonth() + 1 !== month || 274 | date.getFullYear() !== year || 275 | date.getDate() !== day) { 276 | return; 277 | } 278 | return (date); 279 | } 280 | // Calculate the number of days from the first image (06/16/1996) to today 281 | function CalculateDaysFromFirstImage () { 282 | var todaysDate = new Date(); 283 | var firstImageDate = new Date('1996', '06', '16'); 284 | var oneDay = 24 * 60 * 60 * 1000; // hours * minutes * seconds * milliseconds 285 | // (First Date - Second Date) / (one day) 286 | var dateDifference = Math.round(Math.abs((firstImageDate.getTime() - todaysDate.getTime()) / (oneDay))); 287 | return dateDifference; 288 | } 289 | // Generate a random number between min and max 290 | function randomIntFromInterval (min, max) { 291 | return Math.floor(Math.random() * (max - min + 1) + min); 292 | } 293 | // Format a date into the YYYY-MM-DD format 294 | function formatDate (date) { 295 | var d = new Date(date); 296 | var month = '' + (d.getMonth() + 1); 297 | var day = '' + d.getDate(); 298 | var year = d.getFullYear(); 299 | if (month.length < 2) month = '0' + month; 300 | if (day.length < 2) day = '0' + day; 301 | return [year, month, day].join('-'); 302 | } 303 | // Converts a userID into the format recognized by Slack 304 | function makeMention (userId) { 305 | return '<@' + userId + '>'; 306 | }; 307 | // Checks if the message was directed at AstroBot 308 | function isDirect (userId, messageText) { 309 | var userTag = makeMention(userId); 310 | return messageText && 311 | messageText.length >= userTag.length && 312 | messageText.substr(0, userTag.length) === userTag; 313 | }; 314 | slack.login(); 315 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "heroku": { 4 | "url": "url-goes-here", 5 | "checkInterval": 5 6 | }, 7 | 8 | "slack": { 9 | "token": "long-token-key-goes-here" 10 | }, 11 | 12 | "nasa": { 13 | "apiKey": "long-token-key-goes-here" 14 | }, 15 | 16 | "discord": { 17 | "token": "TOKEN HERE" 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | ## Astrobot 2 | A slack bot integration with NASA data 3 | 4 | Copyright 2016-2017 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrobot", 3 | "version": "1.0.0", 4 | "description": "Bot to query and display APOD images.", 5 | "main": "astroBot.js", 6 | "dependencies": { 7 | "discord.js": "^11.2.1", 8 | "express": "^4.13.x", 9 | "request": "^2.60.0", 10 | "slack-client": "^1.4.1", 11 | "strip-indent": "^2.0.0", 12 | "superagent": ">=3.7.0" 13 | }, 14 | "engines": { 15 | "node": "0.12.x" 16 | }, 17 | "devDependencies": {}, 18 | "scripts": { 19 | "start": "node astroBot.js", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [ 23 | "apod", 24 | "bot", 25 | "astro" 26 | ], 27 | "author": "William Baker", 28 | "license": "Apache 2.0" 29 | } 30 | --------------------------------------------------------------------------------