This repository has been archived by the owner on Dec 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* dev: (29 commits) Adjust license to include contributors Minor doc and todo tweaks Run unit & integration tests together so that coverage is correct Add plenty of TODOs for integration tests Add additional unit tests Hotfix: don't pluralise a score of -1 point Minor: update Travis Slack token Additional tests for isValidEvent Split up the app & the server; unit & integration tests; adjust docw Additional contrib docs Lots more feature requests Reorganise somet functionality to make it easier to test Move getRandomMessage to messages.js; minor cleanups Add simple test suite for index.js - incomplete Minor: send 500 rather than 403 when server verification value is not set Add simple test suite for messages.js Workaround jest bug in jsdom/jsdom#2304 Fix yarn upgrade in travis Upgrade yarn in travis Minor docs ...
- Loading branch information
Showing
16 changed files
with
4,325 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
|
||
'use strict'; | ||
|
||
module.exports = { | ||
|
||
env: { | ||
browser: false, | ||
node: true | ||
}, | ||
|
||
extends: [ 'tdmalone' ], | ||
|
||
parserOptions: { | ||
ecmaVersion: 8 | ||
}, | ||
|
||
rules: { | ||
'max-statements': [ 'error', { max: 75 } ], | ||
'no-magic-numbers': [ 'error', { | ||
ignore: [ | ||
-1, | ||
0, | ||
1 | ||
], | ||
ignoreArrayIndexes: true, | ||
enforceConst: true | ||
} ], | ||
'no-multi-str': [ 'off' ] | ||
} | ||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
coverage | ||
docs | ||
node_modules | ||
*.log | ||
package-lock.json | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
language: node_js | ||
node_js: 10.8.0 | ||
|
||
cache: | ||
yarn: true | ||
directories: | ||
- node_modules | ||
|
||
# Upgrade Yarn to the version specified in package.json. | ||
before_install: | ||
- curl --location https://yarnpkg.com/install.sh | bash -s -- --version "$( node -e "console.log(require('./package.json').engines.yarn)" )" | ||
- export PATH="$HOME/.yarn/bin:$PATH" | ||
|
||
install: yarn | ||
|
||
script: | ||
- yarn lint | ||
- yarn test | ||
|
||
# On success, deployments are automatically handled by Heroku. | ||
|
||
notifications: | ||
email: false | ||
slack: | ||
on_start: always | ||
rooms: | ||
- secure: UbCK1YGnopRxELn0sxl1Ra7/c4GjAHYb77FRS3mzgOdIzg+C3VyAilIL1SpAS3ue7IGyG+DeTtVa1azC4Xwo9abpk5/wJQxYBFlBoYV2VO9JWgz74L4i/rW00cbFZDozCv1Df3mwq9OUe6S5eXDY4KSR36hod48aZ/PRwHL8uj/VyYku5JA1BI3nyIQx7N7Tn26jzQkC2M74aO4tN3FSwrYyV0805In43+rUKGnb65glJ+nxrW6A6lBsTH9MiIt2xk9FFBuF9ZQUjG0Fu4asOGROqJp4w5b1p7Hf2PVWB773oFQuNIi4H2jEiHO/QUAtmn8i5lCuzLpB1PuGmcQGIpw4GdZE5R0gZ7au0sgqmiNM20v+5L355M0+bK+BfyEKLz+rBIl0mmmWd1rXipErlhsmQRsp1bvMk7pjoeQGgJ4Qrt9NZYqkmBR/629fGsf68maSNR+AFbYzXeOcSq/6VK445QMdw8XygYiQVswS7Kyu3/s2VeaDJOPUORpJ2jehGsj5a1B0jTpPMYXGVMwYRfA5CqT8BqM0BvwvzHtyz/sGHodMr3UIodBWBIyDigP7Pk8gYN0hTsg6rVZ7SkjWgOKxcuwOnRwA36T9KfVfyEcDfbcyROl3fSTn5wNABBrUxaFU12paTlIQlZ4Gveh9W80xN5y9fHXWJb+B80bHbnA= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
# @see https://devcenter.heroku.com/articles/nodejs-support | ||
web: node index.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,168 +3,26 @@ | |
* Like plusplus.chat, but one that actually works, because you can host it yourself! 😉 | ||
* | ||
* @see https://github.com/tdmalone/working-plusplus | ||
* @see https://api.slack.com/events-api | ||
* @see https://expressjs.com/en/4x/api.html | ||
* @author Tim Malone <[email protected]> | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const express = require( 'express' ), | ||
bodyParser = require( 'body-parser' ), | ||
slackClient = require('@slack/client'), | ||
pg = require( 'pg' ), | ||
messages = require( './messages' ); | ||
|
||
const SLACK_BOT_USER_OAUTH_ACCESS_TOKEN = process.env.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN, | ||
SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN, | ||
DATABASE_URL = process.env.DATABASE_URL; | ||
app = require( './src/app' ); | ||
|
||
// Let Heroku set the port. | ||
const PORT = process.env.PORT || 80; | ||
|
||
const scoresTableName = 'scores'; | ||
|
||
const app = express(), | ||
postgres = new pg.Pool({ connectionString: DATABASE_URL, ssl: true }), | ||
slack = new slackClient.WebClient( SLACK_BOT_USER_OAUTH_ACCESS_TOKEN ); | ||
|
||
const getRandomMessage = ( operation ) => { | ||
operation = operation.replace( '+', 'plus' ).replace( '-', 'minus' ); | ||
max = messages[ operation ].length - 1; | ||
random = Math.floor( Math.random() * max ); | ||
return messages[ operation ][ random ]; | ||
}; | ||
|
||
app.use( bodyParser.json() ); | ||
app.enable( 'trust proxy' ); | ||
|
||
app.get( '/', ( request, response ) => { | ||
response.send( 'It works! However, this app only accepts POST requests for now.' ); | ||
}); | ||
|
||
app.post( '/', async ( request, response ) => { | ||
|
||
console.log( | ||
request.ip + ' ' + request.method + ' ' + request.path + ' ' + request.headers['user-agent'] | ||
); | ||
|
||
// Respond to challenge sent by Slack during event subscription set up. | ||
if ( request.body.challenge ) { | ||
response.send( request.body.challenge ); | ||
console.info( '200 Challenge response sent' ); | ||
return; | ||
} | ||
|
||
// Sanity check for bad verification values. | ||
if ( ! SLACK_VERIFICATION_TOKEN || 'xxxxxxxxxxxxxxxxxxxxxxxx' === SLACK_VERIFICATION_TOKEN ) { | ||
response.status( 403 ).send( 'Access denied.' ); | ||
console.error( '403 Access denied - bad verification value' ); | ||
return; | ||
} | ||
|
||
// Check that this is Slack making the request. | ||
// TODO: Move to calculating the signature instead (newer, more secure method). | ||
if ( SLACK_VERIFICATION_TOKEN !== request.body.token ) { | ||
response.status( 403 ).send( 'Access denied.' ); | ||
console.error( '403 Access denied - incorrect verification token' ); | ||
return; | ||
} | ||
|
||
// Send back a 200 OK now so Slack doesn't get upset. | ||
response.send( '' ); | ||
|
||
const event = request.body.event; | ||
|
||
// Drop events that aren't messages, or that don't have message text. | ||
if ( 'message' !== event.type || ! event.text ) { | ||
console.warn( 'Invalid event received (' + request.event.type + ') or event data missing' ); | ||
return; | ||
} | ||
|
||
// Drop retries. This is controversial. But, because we're mainly gonna be running on free Heroku | ||
// dynos, we'll be sleeping after inactivity. It takes longer than Slack's 3 second limit to start | ||
// back up again, so Slack will retry immediately and then again in a minute - which will result | ||
// in the action being carried out 3 times if we listen to it! | ||
// @see https://api.slack.com/events-api#graceful_retries | ||
if ( request.headers['x-slack-retry-num'] ) { | ||
console.log( 'Skipping Slack retry.' ); | ||
return; | ||
} | ||
const PORT = process.env.PORT || 80; // eslint-disable-line no-process-env, no-magic-numbers | ||
|
||
const text = event.text; | ||
const server = express(); | ||
|
||
// Drop text that doesn't mention anybody/anything. | ||
if ( -1 === text.indexOf( '@' ) ) { | ||
return; | ||
} | ||
|
||
// Drop text that doesn't include ++ or -- (or —, to support iOS replacing --). | ||
if ( -1 === text.indexOf( '++' ) && -1 === text.indexOf( '--' ) && -1 === text.indexOf( '—' ) ) { | ||
return; | ||
} | ||
|
||
// If we're still here, it's a message to deal with! | ||
|
||
// Get the user or 'thing' that is being spoken about, and the 'operation' being done on it. | ||
// We take the operation down to one character, and also support — due to iOS' replacement of --. | ||
const data = text.match( /@([A-Za-z0-9\.\-_]*?)>?\s*([\-+]{2}|—{1})/ ); | ||
const item = data[1]; | ||
const operation = data[2].substring( 0, 1 ).replace( '—', '-' ); | ||
|
||
// If we somehow didn't get anything, drop it. This can happen when eg. @++ is typed. | ||
if ( ! item.trim() ) { | ||
return; | ||
} | ||
|
||
// If the user is trying to ++ themselves... | ||
if ( item === event.user && '+' === operation ) { | ||
|
||
const message = getRandomMessage( 'selfPlus' ); | ||
|
||
slack.chat.postMessage({ | ||
channel: event.channel, | ||
text: '<@' + event.user + '> ' + message, | ||
}).then( ( data ) => { | ||
console.log( | ||
data.ok ? | ||
item + ' tried to alter their own score.' : | ||
'Error occurred posting response to user altering their own score.' | ||
); | ||
}); | ||
|
||
return; | ||
|
||
} | ||
|
||
// Connect to the DB, and create a table if it's not yet there. | ||
const dbClient = await postgres.connect(); | ||
const dbCreateResult = await dbClient.query( 'CREATE EXTENSION IF NOT EXISTS citext; CREATE TABLE IF NOT EXISTS ' + scoresTableName + ' (item CITEXT PRIMARY KEY, score INTEGER);' ); | ||
|
||
// Atomically record the action. | ||
// TODO: Fix potential SQL injection issues here, even though we know the input should be safe. | ||
const dbInsert = await dbClient.query( 'INSERT INTO ' + scoresTableName + ' VALUES (\'' + item + '\', ' + operation + '1) ON CONFLICT (item) DO UPDATE SET score = ' + scoresTableName + '.score ' + operation + ' 1;' ); | ||
|
||
// Get the new value. | ||
// TODO: Fix potential SQL injection issues here, even though we know the input should be safe. | ||
const dbSelect = await dbClient.query( 'SELECT score FROM ' + scoresTableName + ' WHERE item = \'' + item + '\';' ); | ||
const score = dbSelect.rows[0].score; | ||
|
||
dbClient.release(); | ||
|
||
// Respond. | ||
const itemMaybeLinked = item.match( /U[A-Z0-9]{8}/ ) ? '<@' + item + '>' : item; | ||
const pluralise = score === 1 ? '' : 's'; | ||
const message = getRandomMessage( operation ); | ||
slack.chat.postMessage({ | ||
channel: event.channel, | ||
text: ( | ||
message + ' ' + | ||
'*' + itemMaybeLinked + '* is now on ' + score + ' point' + pluralise + '.' | ||
) | ||
}).then( ( data ) => { | ||
console.log( data.ok ? item + ' now on ' + score : 'Error occurred posting response.' ); | ||
}); | ||
|
||
}); | ||
server.use( bodyParser.json() ); | ||
server.enable( 'trust proxy' ); | ||
server.get( '/', app.handleGet ); | ||
server.post( '/', app.handlePost ); | ||
|
||
app.listen( PORT, () => { | ||
console.log( 'Listening on port ' + PORT + '.' ) | ||
server.listen( PORT, () => { | ||
console.log( 'Listening on port ' + PORT + '.' ); | ||
}); |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.