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:
  Add a small selection of random response messages
  Properly fix Slack retry skip!
  Hotfix: different approach to checking the retry headers
  Minor tweaks only
  Lots more docs
  Minor app.json tweaks
  Minor tweaks only
  Add support for Slack on iOS replacing -- with —
  Drop retries to support both Slack & Heroku free dynos
  Allow PORT to be overridden by environment
  Initial commit of basic app
  Additional docs
  Installation instructions + Heroku app.json
  • Loading branch information
tdmalone committed Aug 6, 2018
2 parents 20fbd9a + f0a8097 commit 05277d4
Show file tree
Hide file tree
Showing 7 changed files with 1,280 additions and 1 deletion.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node index.js
86 changes: 85 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,88 @@

Like [plusplus.chat](https://plusplus.chat/), except this one actually works - because you can host it yourself! 😉

**UNDER CONSTRUCTION.** Despite the above display of confidence, this app will actually _not_ work yet, because it's still under construction. Come back soon, or check out the [`dev` branch](https://github.com/tdmalone/working-plusplus/tree/dev) if you're brave.
As PlusPlus++ says:

> Plus, minus, and keep score of all the good and not so good things your friends say and do on Slack.
It's as simple as writing in Slack:

```@Tim++ for being awesome!```

Or:

```@Cheeseburgers++```

Or:

```@CoffeeShop-- for forgetting my order ;(```

**Working PlusPlus++** will keep track of the score everyone (and everything) is sitting on.

Completely open source, so do with it what you like. Or if you don't want to make your own tweaks, deploy it as-is right now with the instructions below. You need somewhere to host it: [Heroku](https://heroku.com) is highly recommended because it's free in most cases, and performs super well.

## Installation

1. **Create a new app in your Slack team.**

You can do this from the [Slack API Apps page](https://api.slack.com/apps). You'll need permission to add new apps, which depending on your team settings might require an admin to do it for you.

1. **Add a bot user for your app.**

This can be done under *Bot Users* in the menu on the left. You can name it whatever you like, and for best results, select it to always show as online.

This allows the app to speak back to your team when they ++ and -- things.

1. **Add chat permissions, and install the app.**

Under *OAuth & Permissions*, scroll down to *Scopes* and add the `chat:write:bot` permission. Click *Save Changes*.

You can now install the app. Scroll back up, click *Install App to Workspace*, and follow the prompts.

1. **Copy your tokens.**

From the same *OAuth & Permissions* page, copy the ***Bot** User OAuth Access Token* (_not_ the non-bot token!) and store it somewhere.

Go back to the *Basic Information* page, scroll down, and copy the *Verification Token* too.

1. **Deploy the app somewhere.**

Heroku is recommended because it's simple and easy, and on most Slack teams this should not cost you a cent.

[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

If you need to sign up first, do so, then come back here and click the Deploy button again.

Find out more about Heroku [here](https://www.heroku.com/about) or [here](https://devcenter.heroku.com/), and Heroku Postgres [here](https://www.heroku.com/postgres) or [here](https://elements.heroku.com/addons/heroku-postgresql).

To increase the free hours available to your account you may need to add a credit card. The hours used by the app will vary depending on the activity on your Slack account, but it won't cost you anything unless you upgrade your plan to support increased scale (or unless you have other Heroku apps using your hours!). The Postgres addon (for storing the scores) is also free for up to 10,000 rows (that's 10,000 unique users or things that your team can ++ or --).

Hosting somewhere other than Heroku is fine too. See *Detailed Instructions* below.

1. **Back at Slack apps, switch on *Event Subscriptions* for your app.**

Via *Event Subscriptions* in the left menu. After switching on, enter your new Heroku app address - eg. `https://my-plusplus.herokuapp.com` - as the request URL.

Scroll down and, under *Subscribe to Bot Events*, add the `message.channels` and `message.groups` events, then click *Save Changes*.

1. **Invite your new bot to any channel in your Slack team.**

1. **Think of someone who's been awesome lately and send `@Someone++`!**

## Detailed Instructions

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

## TODO

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

* Add tests
* Add a leaderboard
* Add the ability to customise the messages the bot sends back
* 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!

## License

[MIT](LICENSE).
28 changes: 28 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "Working PlusPlus++",
"description": "Like plusplus.chat, but one that actually works, because you can host it yourself 😉",
"website": "https://github.com/tdmalone/working-plusplus",
"repository": "https://github.com/tdmalone/working-plusplus",
"keywords": [
"slack",
"karma"
],
"env": {
"SLACK_BOT_USER_OAUTH_ACCESS_TOKEN": {
"description": "Token provided by Slack for your bot user when installing the app to your team. Find this under Basic Information -> OAuth & Permissions within your Slack app management page.",
"value": "xoxp-00000000000-000000000000-xxxxxxxxxxxxxxxxxxxxxxxx"
},
"SLACK_VERIFICATION_TOKEN": {
"description": "Verification Token provided by Slack. Find this under Basic Information -> App Credentials within your Slack app management page.",
"value": "xxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"addons": [
"heroku-postgresql"
],
"buildpacks": [
{
"url": "heroku/nodejs"
}
]
}
170 changes: 170 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Working PlusPlus++
* 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
* @author Tim Malone <[email protected]>
*/

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;

// 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 text = event.text;

// 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.' );
});

});

app.listen( PORT, () => {
console.log( 'Listening on port ' + PORT + '.' )
});
47 changes: 47 additions & 0 deletions messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Provides messages for random selection.
*
* TODO: Add the ability to customise these messages - probably via JSON objects in environment
* variables.
*/

module.exports = {

plus: [
"Congrats!",
"Got it!",
"Bravo.",
"Oh well done.",
"Nice work!",
"Well done.",
"Exquisite.",
"Lovely.",
"Superb.",
"Classic!",
"Charming.",
"Noted.",
"Well, well!",
"Well played.",
"Sincerest congratulations.",
"Delicious."
],

minus: [
"Oh RLY?",
"Oh, really?",
"Oh :slightly_frowning_face:.",
"I see.",
"Ouch.",
"Oh là là.",
"Oh.",
"Condolences."
],

selfPlus: [
"Hahahahahahaha no.",
"Nope.",
"No. Just no.",
"Not cool!"
]

};
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "working-plusplus",
"version": "0.0.1",
"description": "Like plusplus.chat, but one that actually works, because you can host it yourself 😉",
"main": "index.js",
"repository": "[email protected]:tdmalone/working-plusplus.git",
"author": "Tim Malone <[email protected]>",
"license": "MIT",
"dependencies": {
"@slack/client": "^4.3.1",
"body-parser": "^1.18.3",
"express": "^4.16.3",
"pg": "^7.4.3"
}
}
Loading

0 comments on commit 05277d4

Please sign in to comment.