-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 15db9e5
Showing
19 changed files
with
10,021 additions
and
0 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,2 @@ | ||
node_modules | ||
README.md |
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,2 @@ | ||
node_modules | ||
coverage |
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,4 @@ | ||
FROM node:19-alpine3.16 | ||
COPY . . | ||
RUN npm ci | ||
CMD ["npm", "start"] |
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,36 @@ | ||
[](https://shields.io/) | ||
|
||
# Slack Setup | ||
1. [Create a new slack app](https://api.slack.com/apps/new) | ||
2. Copy `Signing Secret` | ||
3. Go to `Features > Oauth & Permissions` and grant the following permissions: | ||
1. `channels:history`: the bot needs to be able to read messages that are reacted to | ||
<!-- TODO: verify this is needed --> | ||
2. `chat:write`: for the bot to write messages | ||
3. `reactions:read`: so that the bot can react to reaction events | ||
4. Install app in workspace | ||
5. Copy the `Bot User Oauth Toekn` | ||
6. Enable `Socket Mode` in `Settings > Socket Mode` | ||
1. this should force you to create an app token and add the `connections:write` scope | ||
7. Copy the newly created `App-Level Token` in `Settings > Basic Information` | ||
8. Go to `Features > Event Subscriptions` and enable the following `bot events`: | ||
1. `message.channels`: so that the bot can react to messages sent in channels | ||
2. `reaction_added`: so the bot can react to reactions added to messages | ||
9. Add a slack command for `/leaderboard` | ||
|
||
# Build and Deploy the App | ||
|
||
|
||
# Local Development | ||
1. export the following environment variables (copied when creating the app): | ||
1. `SLACK_BOT_TOKEN` | ||
2. `SLACK_SIGNING_SECRET` | ||
3. `SLACK_APP_TOKEN` | ||
2. run the app with `npm start` | ||
|
||
## Docker | ||
1. export the following environment variables (copied when creating the app): | ||
1. `SLACK_BOT_TOKEN` | ||
2. `SLACK_SIGNING_SECRET` | ||
3. `SLACK_APP_TOKEN` | ||
2. run the app with `npm run docker-build-and-run` |
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,55 @@ | ||
const { App } = require("@slack/bolt"); | ||
const { Avokudos } = require("./lib/avokudos"); | ||
const { LocalKeeper } = require("./lib/keepers/local"); | ||
const { RedisKeeper } = require("./lib/keepers/redis"); | ||
const redis = require("redis"); | ||
|
||
let keeper; | ||
if (process.env.REDIS_HOST != undefined) { | ||
console.log("REDIS_HOST is set. Using RedisKeeper"); | ||
const redis_client = redis.createClient({ | ||
socket: { | ||
host: process.env.REDIS_HOST, | ||
port: process.env.REDIS_PORT || 6379, | ||
}, | ||
password: process.env.REDIS_PASSWORD, | ||
}); | ||
|
||
redis_client.on("error", (err) => { | ||
console.log("Redis Error: " + err); | ||
}); | ||
redis_client | ||
.connect() | ||
.then(() => { | ||
console.log("Connected to redis instance"); | ||
}) | ||
.catch((err) => { | ||
console.error("Failed to connect to redis: " + err); | ||
rocess.exit(1); | ||
}); | ||
|
||
keeper = new RedisKeeper(redis_client); | ||
} else { | ||
console.log("Using LocalKeeper"); | ||
keeper = new LocalKeeper(); | ||
} | ||
|
||
const avokudos = new Avokudos(keeper); | ||
|
||
const app = new App({ | ||
token: process.env.SLACK_BOT_TOKEN, | ||
signingSecret: process.env.SLACK_SIGNING_SECRET, | ||
socketMode: true, | ||
appToken: process.env.SLACK_APP_TOKEN, | ||
}); | ||
|
||
app.message(":avocado:", avokudos.hearMessage); | ||
app.event("reaction_added", avokudos.hearReactionAdded); | ||
app.event("reaction_removed", avokudos.hearReactionRemoved); | ||
|
||
app.command("/leaderboard", avokudos.getLeaderboard); | ||
|
||
(async () => { | ||
await app.start(process.env.PORT || 3000); | ||
console.log("🥑 Avokudos app is running!"); | ||
})(); |
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,12 @@ | ||
module.exports = { | ||
collectCoverage: true, | ||
collectCoverageFrom: ["**/*.js"], | ||
verbose: true, | ||
coveragePathIgnorePatterns: [ | ||
"node_modules", | ||
"test", | ||
"coverage", | ||
"jest.config.js", | ||
"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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
This dir contains k8s manifests for deploying avokudos to your cluster. Since its a simple app that connects to slack | ||
via sockets and it doesn't take any other traffic, it just needs a deployment. Feel free to customize as you need. | ||
|
||
|
||
For redis, its probably best to use a helm chart. See https://github.com/bitnami/charts/tree/main/bitnami/redis for | ||
bitnami's redis helm chart. I'd recommend making sure you have your redis instance use some kind of persistant volume | ||
so that you don't lost your kudos tracking. By default, bitnamis redis chart sets this up for you. |
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,66 @@ | ||
apiVersion: apps/v1 | ||
kind: Deployment | ||
metadata: | ||
name: avokudos-deployment | ||
labels: | ||
app: avokudos | ||
spec: | ||
replicas: 1 | ||
selector: | ||
matchLabels: | ||
app: avokudos | ||
template: | ||
metadata: | ||
labels: | ||
app: avokudos | ||
spec: | ||
containers: | ||
- name: avokudos | ||
image: avokudos:1.0.0 | ||
resources: | ||
# TODO: tune this | ||
requests: | ||
cpu: 100m | ||
memory: 256Mi | ||
limits: | ||
cpu: 500m | ||
memory: 1Gi | ||
env: | ||
- name: SLACK_APP_TOKEN | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: SLACK_APP_TOKEN | ||
optional: false | ||
- name: SLACK_BOT_TOKEN | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: SLACK_BOT_TOKEN | ||
optional: false | ||
- name: SLACK_SIGNING_SECRET | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: SLACK_SIGNING_SECRET | ||
optional: false | ||
|
||
# If REDIS_HOST is not set, avokudos will use in memory storage for tracking kudos | ||
- name: REDIS_HOST | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: REDIS_HOST | ||
optional: true | ||
- name: REDIS_PORT | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: REDIS_PORT | ||
optional: true | ||
- name: REDIS_PASSWORD | ||
valueFrom: | ||
secretKeyRef: | ||
name: avokudos | ||
key: REDIS_HOST | ||
optional: true |
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,111 @@ | ||
const { IDFromMention, mentionFromID } = require("./user_id"); | ||
|
||
class Avokudos { | ||
constructor(keeper) { | ||
this.keeper = keeper; | ||
} | ||
|
||
sendUserMessage = async (client, user_id, text) => { | ||
await client.chat.postMessage({ | ||
channel: IDFromMention(user_id), | ||
text: text, | ||
}); | ||
}; | ||
|
||
sendUserKudosNotification = async (client, to_user_id, from_user_id) => { | ||
const current_kudos = await this.keeper.getUserKudos( | ||
mentionFromID(to_user_id) | ||
); | ||
await this.sendUserMessage( | ||
client, | ||
IDFromMention(to_user_id), | ||
`You received avokudos from ${mentionFromID( | ||
from_user_id | ||
)}!\nYou now have ${current_kudos} :avocado:` | ||
); | ||
}; | ||
|
||
getMessage = async (client, channel, ts) => { | ||
const messages = await client.conversations.history({ | ||
channel: channel, | ||
latest: ts, | ||
inclusive: true, | ||
limit: 1, | ||
}); | ||
|
||
return messages.messages[0].text; | ||
}; | ||
|
||
getMentionedUsers = (text) => { | ||
return text.match(/<@\w+>/g); | ||
}; | ||
|
||
hearMessage = async (res) => { | ||
const { message, client } = res; | ||
const mentioned_users = this.getMentionedUsers(message.text); | ||
for (const user of mentioned_users) { | ||
if (IDFromMention(user) != IDFromMention(message.user)) { | ||
await this.keeper.giveUserKudos(user); | ||
await this.sendUserKudosNotification(client, user, message.user); | ||
} | ||
} | ||
}; | ||
|
||
hearReactionAdded = async (res) => { | ||
const { client, event } = res; | ||
const message = await this.getMessage( | ||
client, | ||
event.item.channel, | ||
event.item.ts | ||
); | ||
const mentioned_users = this.getMentionedUsers(message); | ||
if (mentioned_users?.length > 0) { | ||
for (const user of mentioned_users) { | ||
if (IDFromMention(user) != IDFromMention(event.user)) { | ||
await this.keeper.giveUserKudos(user); | ||
await this.sendUserKudosNotification(client, user, event.user); | ||
} | ||
} | ||
} else { | ||
if (IDFromMention(event.item_user) != IDFromMention(event.user)) { | ||
await this.keeper.giveUserKudos(event.item_user); | ||
await this.sendUserKudosNotification( | ||
client, | ||
event.item_user, | ||
event.user | ||
); | ||
} | ||
} | ||
}; | ||
|
||
hearReactionRemoved = async (res) => { | ||
const { client, event } = res; | ||
const message = await this.getMessage( | ||
client, | ||
event.item.channel, | ||
event.item.ts | ||
); | ||
const mentioned_users = this.getMentionedUsers(message); | ||
if (mentioned_users?.length > 0) { | ||
for (const user of mentioned_users) { | ||
if (IDFromMention(user) != IDFromMention(event.user)) { | ||
await this.keeper.removeUserKudos(user); | ||
} | ||
} | ||
} else { | ||
if (IDFromMention(event.item_user) != IDFromMention(event.user)) { | ||
await this.keeper.removeUserKudos(event.item_user); | ||
} | ||
} | ||
}; | ||
|
||
getLeaderboard = async (res) => { | ||
const { ack, respond } = res; | ||
await ack(); | ||
await respond(await this.keeper.getLeaderboard()); | ||
}; | ||
} | ||
|
||
module.exports = { | ||
Avokudos, | ||
}; |
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,47 @@ | ||
const { IDFromMention, mentionFromID } = require("../user_id"); | ||
|
||
class LocalKeeper { | ||
constructor() { | ||
this.keeper = {}; | ||
} | ||
|
||
getUserKudos = async (user_id) => { | ||
return this.keeper[IDFromMention(user_id)]; | ||
}; | ||
|
||
giveUserKudos = async (user_id) => { | ||
const id = IDFromMention(user_id); | ||
if (this.keeper[id] !== undefined) { | ||
this.keeper[id]++; | ||
} else { | ||
this.keeper[id] = 1; | ||
} | ||
}; | ||
|
||
removeUserKudos = async (user_id) => { | ||
const id = IDFromMention(user_id); | ||
if (id in this.keeper) { | ||
if (this.keeper[id] > 0) { | ||
this.keeper[id]--; | ||
} | ||
} | ||
}; | ||
|
||
getLeaderboard = async () => { | ||
let response = "Avokudos Leaderboard:\n"; | ||
if (Object.keys(this.keeper).length == 0) { | ||
return "No avocados have been given out yet!"; | ||
} | ||
const sorted = Object.entries(this.keeper).sort(([, a], [, b]) => b - a); | ||
let i = 1; | ||
for (let k of sorted) { | ||
response += `${i}. ${mentionFromID(k[0])}: ${k[1]}\n`; | ||
i++; | ||
} | ||
return response; | ||
}; | ||
} | ||
|
||
module.exports = { | ||
LocalKeeper, | ||
}; |
Oops, something went wrong.