Skip to content
This repository has been archived by the owner on Dec 10, 2022. It is now read-only.

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
* 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
tdmalone committed Aug 8, 2018
2 parents 05277d4 + 4a67e64 commit e99a27b
Show file tree
Hide file tree
Showing 16 changed files with 4,325 additions and 278 deletions.
31 changes: 31 additions & 0 deletions .eslintrc.js
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' ]
}

};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
coverage
docs
node_modules
*.log
package-lock.json
.env
27 changes: 27 additions & 0 deletions .travis.yml
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=
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2018 Tim Malone
Copyright (c) 2018 Tim Malone and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 1 addition & 0 deletions Procfile
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
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,65 @@ Completely open source, so do with it what you like. Or if you don't want to mak

Further instructions, such as hosting elsewhere, upgrading, etc. are coming soon.

## Contributing

Contributions are welcome! [Create an issue](https://github.com/tdmalone/working-plusplus/issues/new) if there's something you'd like to see or [send a pull request](https://github.com/tdmalone/working-plusplus/compare) if you can implement it yourself.

To develop locally, follow most of the *Installation* instructions above, except **instead of step 5** (deploying to Heroku), clone this repo locally and then install dependencies:

$ git clone https://github.com/tdmalone/working-plusplus
$ cd working-plusplus
$ yarn

You'll need [Node.js](https://nodejs.org/) already installed on your system. In addition, if you don't have [Yarn](https://yarnpkg.com/en/) and don't want it, you can use `npm install` instead of `yarn` above (but you might not get _exactly_ the same dependency versions).

After installing, to run the app:

$ node index.js

Or if you have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed:

$ heroku local

_For more help on running `heroku local`, see the [Heroku Local Dev Center article](https://devcenter.heroku.com/articles/heroku-local)._

If you make changes to the app, press Ctrl+C to exit, and then run it again.

Finally, you'll need to be able to have Slack contact your development instance directly. If you don't have the ability to forward a port through to your machine, I recommend [ngrok](https://ngrok.com/). Download and extract, then in the directory you've extracted it in run:

./ngrok http 80 # Or port 5000 if you're running with heroku local.

ngrok will provide you with the public URL your app is accessible on. This is the URL you'll then need to use in **step 6** of the installation instructions above.

Other than the modifications to steps 5 and 6, make sure you've followed all the rest of the installation instructions. You should then be set up and ready with a local development instance that you can interact with directly on Slack! If you run into any problems, feel free to [create an issue](https://github.com/tdmalone/working-plusplus/issues/new).

### Linting and Running Tests

Before submitting pull requests, please check that your changes pass linting and tests by running `yarn lint` and `yarn test`. These will also be run for you by Travis CI, but it's often quicker to debug and resolve the issues locally.

You can run just the unit tests with `yarn unit-tests`, and just the integration tests with `yarn integration-tests`. It is normal to see errors while running the integration tests - some of the tests specifically check for these errors - but keep an eye on the exit code of the process to determine if it is successful (run `echo $?` immediately after running `yarn test` - you're looking for an exit code of `0` for a pass.)

You can modify the default testing behaviour by adjusting the relevant `scripts` in [`package.json`](package.json) or in some cases by passing additional [Jest configuration parameters](https://jestjs.io/docs/en/configuration.html) on the command line.

If you come across annoying *stylistic* linting rules, feel free to [change them](https://eslint.org/docs/rules/) in [`.eslintrc.js`](.eslintrc.js) as part of your pull request, providing they don't cause an adverse effect on existing code. Many linting issues can be automatically fixed by running `yarn fix`.

## TODO

Although it works, it's very basic. Enhancements include:
Although it works, it's very basic. Potential enhancements include:

* Add tests
* Add a leaderboard
* Add the ability to customise the messages the bot sends back
* Improve tests for much better coverage **(in progress on [#dev](https://github.com/tdmalone/working-plusplus/tree/dev))**
* A way to retrieve the current version/git hash from Slack, for sanity-checking of deployments
* Leaderboard functionality (either, or both, via a full leaderboard on the web - with some sort of token or oauth - and a shorter leaderboard via a command in Slack)
* The ability to customise the messages the bot sends back at runtime (eg. via environment variables)
* Move to the newer, more secure method of calculating signatures for incoming Slack hooks
* Something you'd like to see? [Create an issue](https://github.com/tdmalone/working-plusplus/issues/new) or [send a pull request](https://github.com/tdmalone/working-plusplus/compare) if you can implement it yourself!
* A way to look up someone's karma without necessarily `++`'ing or `--`'ing them (eg. `@username==`)
* Support for posting back messages within threads, rather than automatically jumping back out to the channel
* Support for detecting multiple actions within one message
* Natural language processing to figure out positive and negative sentiment automatically
* Option to deduct karma instead of adding karma when someone tries to give themselves karma
* Option to deduct karma automatically for swearing (with customisable word list?)
* Record and make accessible how many karma points someone has _given_
* Enhance messages to support interpolation of variables such as score and name of user or thing **(in progress, see [#1](https://github.com/tdmalone/working-plusplus/pull/1))**

## License

Expand Down
4 changes: 4 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"SLACK_VERIFICATION_TOKEN": {
"description": "Verification Token provided by Slack. Find this under Basic Information -> App Credentials within your Slack app management page.",
"value": "xxxxxxxxxxxxxxxxxxxxxxxx"
},
"YARN_PRODUCTION": {
"description": "This configures Heroku to only install production dependencies. See https://devcenter.heroku.com/articles/nodejs-support#package-installation for more details.",
"value": true
}
},
"addons": [
Expand Down
166 changes: 12 additions & 154 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '.' );
});
47 changes: 0 additions & 47 deletions messages.js

This file was deleted.

Loading

0 comments on commit e99a27b

Please sign in to comment.