Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jrc356 committed Dec 11, 2022
0 parents commit 15db9e5
Show file tree
Hide file tree
Showing 19 changed files with 10,021 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
README.md
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
coverage
4 changes: 4 additions & 0 deletions Dockerfile
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"]
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[![Generic badge](https://img.shields.io/badge/Status-Under%20Development-yellow.svg)](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`
55 changes: 55 additions & 0 deletions index.js
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!");
})();
12 changes: 12 additions & 0 deletions jest.config.js
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",
],
};
7 changes: 7 additions & 0 deletions k8s/README.md
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.
66 changes: 66 additions & 0 deletions k8s/deployment.yml
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
111 changes: 111 additions & 0 deletions lib/avokudos.js
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,
};
47 changes: 47 additions & 0 deletions lib/keepers/local.js
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,
};
Loading

0 comments on commit 15db9e5

Please sign in to comment.