Skip to content

Commit de6d2c8

Browse files
committed
feat(slackbot): do some backend work for the Slack app
In this commit: * add some skelton library code for data persistence via Prisma ORM and Postgres * add some word-based triggers for some automated responses a la Slackbot * copious amounts of Block Kit Builder usage for building views Pardon the mega commit dump!
1 parent 0fd0f92 commit de6d2c8

File tree

16 files changed

+418
-79
lines changed

16 files changed

+418
-79
lines changed

packages/slackbot/app.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { App, LogLevel } from '@slack/bolt';
22
import registerListeners from './listeners';
3+
import { prisma } from './lib/db';
34

45
/** Initialization */
56
const app = new App({
@@ -16,12 +17,16 @@ registerListeners(app);
1617
(async () => {
1718
try {
1819
await app.start(process.env.PORT || 33123);
19-
console.log('⚡️ Bolt app is running! ⚡️');
20+
await prisma.$connect()
21+
console.log("Now connected to database for data presistence")
22+
console.log('⚡️ Recap Time Bot is up at port', process.env.PORT);
2023
app.client.users.setPresence({
2124
presence: 'auto',
2225
token: process.env.SLACK_BOT_TOKEN,
2326
})
2427
} catch (error) {
2528
console.error('Unable to start App', error);
29+
await prisma.$disconnect()
30+
process.exit(1)
2631
}
2732
})();

packages/slackbot/lib/blocks/homepage_nonstaff.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { HomeView } from '@slack/bolt';
2+
13
export default function homepageNonStaffView(
24
uid: string,
35
username?: string | null,
4-
) {
6+
): HomeView {
57
return {
68
type: 'home' as const,
9+
callback_id: 'home_community-unverified',
710
blocks: [
811
{
912
type: 'header',

packages/slackbot/lib/blocks/request_permissions.ts

Lines changed: 69 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,141 @@
1-
import { ModalView, View } from "@slack/bolt";
1+
import { ModalView } from "@slack/bolt";
22

3-
const requestPermissionsPrompt: View = {
4-
type: 'modal',
3+
const requestPermissionsPrompt: ModalView = {
4+
callback_id: "requestPermSubmission",
5+
type: "modal",
56
title: {
6-
type: 'plain_text',
7-
text: 'Request permissions',
7+
type: "plain_text",
8+
text: "Request permissions",
89
emoji: true,
910
},
1011
submit: {
11-
type: 'plain_text',
12-
text: 'Submit request',
12+
type: "plain_text",
13+
text: "Submit request",
14+
1315
emoji: true,
1416
},
1517
close: {
16-
type: 'plain_text',
17-
text: 'Cancel',
18+
type: "plain_text",
19+
text: "Cancel",
1820
emoji: true,
1921
},
2022
blocks: [
2123
{
22-
type: 'input',
23-
block_id: 'role',
24+
type: "input",
25+
block_id: "role",
2426
element: {
25-
type: 'static_select',
27+
type: "static_select",
2628
placeholder: {
27-
type: 'plain_text',
28-
text: 'Select role here',
29+
type: "plain_text",
30+
text: "Select role here",
2931
emoji: true,
3032
},
3133
options: [
3234
{
3335
text: {
34-
type: 'plain_text',
35-
text: 'Squad member',
36+
type: "plain_text",
37+
text: "Squad member",
3638
emoji: true,
3739
},
38-
value: 'crew.recaptime.dev',
40+
value: "crew.recaptime.dev",
3941
},
4042
{
4143
text: {
42-
type: 'plain_text',
43-
text: 'Hack Clubber from HQ or HCB',
44+
type: "plain_text",
45+
text: "Hack Clubber from HQ or HCB",
4446
emoji: true,
4547
},
46-
value: 'hackclub.com/team',
48+
value: "hackclub.com/team",
4749
},
4850
{
4951
text: {
50-
type: 'plain_text',
51-
text: 'Community maintainers + mods (including community mods/Hack Club Slack FD)',
52+
type: "plain_text",
53+
text: "Community maintainers + mods (including community mods/Hack Club Slack FD)",
5254
emoji: true,
5355
},
54-
value: 'crew.recaptime.dev/community-maintainers',
56+
value: "crew.recaptime.dev/community-maintainers",
5557
},
5658
{
5759
text: {
58-
type: 'plain_text',
59-
text: 'None of the above',
60+
type: "plain_text",
61+
text: "None of the above",
6062
emoji: true,
6163
},
62-
value: 'regular-user',
64+
value: "regular-user",
6365
},
6466
],
65-
action_id: 'selector',
67+
action_id: "selector",
6668
},
6769
label: {
68-
type: 'plain_text',
69-
text: 'Are you RecapTime.dev staff or community maintainer?',
70+
type: "plain_text",
71+
text: "Are you RecapTime.dev staff or community maintainer?",
7072
emoji: true,
7173
},
74+
optional: false,
7275
},
7376
{
74-
type: 'context',
77+
type: "context",
7578
elements: [
7679
{
77-
type: 'mrkdwn',
78-
text: "To unlock the rest of @recaptimebot's features, you must be a Recap Time Squad member (part of @recaptimesquad user group), Hack Club Staff (@staff) or HCB Staff (@hcbops, @hcbteam or @hcb-engineers), a community maintainer from one of our projects (or from a open-source project we recongized), or a community moderator on one of our community spaces (this includes the Fire Department (@fire-fighters) at Hack Club Slack).",
80+
type: "mrkdwn",
81+
text: "To unlock more of Recap Time Bot features, you must be a <https://crew.recaptime.dev|current squad member> or <https://wiki.recaptime.dev/handbook/access-permission-levels#eligibility|part of eligible groups>.",
7982
},
8083
],
8184
},
8285
{
83-
type: 'input',
84-
block_id: 'codeforge_profile',
86+
type: "input",
87+
block_id: "codeforge_profile",
8588
element: {
86-
type: 'plain_text_input',
87-
action_id: 'username',
89+
type: "plain_text_input",
90+
action_id: "username",
8891
},
8992
label: {
90-
type: 'plain_text',
91-
text: 'Your GitHub, GitLab SaaS or sourcehut username',
93+
type: "plain_text",
94+
text: "Your GitHub, GitLab SaaS or sourcehut username",
9295
emoji: true,
9396
},
97+
hint: {
98+
type: "plain_text",
99+
text: "https://code-forge.tld/username",
100+
},
101+
optional: false,
94102
},
95103
{
96-
type: 'context',
104+
type: "context",
97105
elements: [
98106
{
99-
type: 'mrkdwn',
100-
text: "If you don't use GitHub but using either the GitLab SaaS instance (`gitlab.com`) or sourcehut (`sr.ht`), please fill this in.",
107+
type: "mrkdwn",
108+
text: "If you self-host GitLab, sourcehut or other open-source git hosting service, please paste the full profile URL here.",
101109
},
102110
],
103111
},
104112
{
105-
type: 'input',
106-
block_id: 'reason',
113+
type: "input",
114+
block_id: "reason",
107115
element: {
108-
type: 'plain_text_input',
116+
type: "plain_text_input",
109117
multiline: true,
110-
action_id: 'content',
118+
action_id: "content",
111119
},
112120
label: {
113-
type: 'plain_text',
114-
text: 'Request reason',
121+
type: "plain_text",
122+
text: "Request reason",
115123
emoji: true,
116124
},
125+
hint: {
126+
text: "Your reason here",
127+
type: "plain_text",
128+
},
129+
optional: false,
130+
},
131+
{
132+
type: "context",
133+
elements: [
134+
{
135+
type: "mrkdwn",
136+
text: "We use this information to help use review your request and grant you access faster.",
137+
},
138+
],
117139
},
118140
],
119141
};

packages/slackbot/lib/db.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,76 @@
1-
import { PrismaClient } from '@prisma/client';
1+
// eslint-disable-next-line import/no-relative-packages
2+
import { PrismaClient } from "../prisma/lib";
3+
import { UserInfo, UserRole } from "./types";
24

3-
const prisma = new PrismaClient();
5+
export const prisma = new PrismaClient();
6+
7+
type UserLookupResult = {
8+
result: UserInfo | null
9+
error?: object | string | unknown
10+
}
11+
12+
export async function lookupUser(id: string): Promise<UserLookupResult> {
13+
try {
14+
const data = await prisma.slackUser.findUnique({
15+
where: {
16+
user_id: id,
17+
},
18+
});
19+
20+
if (data == null) {
21+
return {
22+
result: null,
23+
error: {
24+
code: "USER_NOT_FOUND",
25+
message: "Slack user not found",
26+
},
27+
};
28+
}
29+
return {
30+
result: {
31+
user_id: data.user_id,
32+
roles: data.roles as UserRole[],
33+
is_staff: data.is_staff || false,
34+
created_on: data.created_on,
35+
updated_at: data.updated_at,
36+
},
37+
error: undefined,
38+
};
39+
} catch (error) {
40+
console.log(error);
41+
return {
42+
result: null,
43+
error,
44+
};
45+
}
46+
}
47+
48+
export async function linkUserToTeam(user_id: string, team: string) {
49+
try {
50+
const { roles: existingRoles } = await prisma.slackUser.findUnique({
51+
select: {
52+
roles: true,
53+
},
54+
where: {
55+
user_id,
56+
},
57+
}) || { roles: [] };
58+
59+
const updatedRoles = [...new Set([...existingRoles, ...team])];
60+
61+
const result = await prisma.slackUser.update({
62+
where: { user_id },
63+
data: { roles: updatedRoles },
64+
});
65+
return {
66+
result,
67+
error: null,
68+
};
69+
} catch (error) {
70+
console.log(error);
71+
return {
72+
result: null,
73+
error,
74+
};
75+
}
76+
}

packages/slackbot/lib/types.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Acceptable user roles
2+
export type UserRole = "community" | "crew.recaptime.dev" | "hackclub.com/team"
3+
4+
/**
5+
* Slack user information
6+
*/
7+
export interface UserInfo {
8+
// Slack user ID
9+
user_id: string
10+
11+
roles?: UserRole[]
12+
13+
// Whether if a user is a Recap Time Squad squad member or Hack Club HQ or HCB staff.
14+
is_staff: boolean
15+
16+
// Whether the user is banned from using the bot
17+
banned?: boolean
18+
ban_reason?: string
19+
20+
// eslint-disable-next-line no-use-before-define
21+
permission_requests?: BotPermissionRequest[]
22+
23+
created_on: Date
24+
updated_at: Date
25+
}
26+
27+
export interface BotPermissionRequest {
28+
id: string,
29+
user: UserInfo, // This references UserInfo, which is now defined before this type
30+
user_id: UserInfo["user_id"],
31+
created_on: Date,
32+
updated_on: Date
33+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { App } from '@slack/bolt';
22
import sampleActionCallback from './sample-action';
3-
import requestAccessCallback from './request-access';
3+
import { requestAccessCallback, requestPermSubmissionHandler } from './request-access';
44

55
const register = (app: App) => {
66
app.action('sample_action_id', sampleActionCallback);
77
app.action('requestAccessPrompt', requestAccessCallback);
8+
app.action('requestPermSubmission', requestPermSubmissionHandler);
89
};
910

1011
export default { register };

packages/slackbot/listeners/actions/request-access.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { AllMiddlewareArgs, BlockAction, SlackActionMiddlewareArgs } from '@slack/bolt';
22
import requestPermissionsPrompt from '../../lib/blocks/request_permissions';
33

4-
const requestAccessCallback = async ({ ack, client, body }:
4+
export const requestAccessCallback = async ({ ack, client, body }:
55
AllMiddlewareArgs & SlackActionMiddlewareArgs<BlockAction>) => {
66
try {
77
await ack();
8-
await client.views.update({
8+
await client.views.open({
99
trigger_id: body.trigger_id,
1010
view: requestPermissionsPrompt,
11-
});
11+
})
1212
} catch {
1313
await client.views.update({
1414
trigger_id: body.trigger_id,
@@ -39,4 +39,11 @@ const requestAccessCallback = async ({ ack, client, body }:
3939
}
4040
};
4141

42-
export default requestAccessCallback;
42+
export const requestPermSubmissionHandler = async({ ack, client, body }:
43+
AllMiddlewareArgs & SlackActionMiddlewareArgs<BlockAction>) => {
44+
try {
45+
const { state, user } = body;
46+
} catch (error) {
47+
console.error(error);
48+
}
49+
}

packages/slackbot/listeners/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import botCommandHandler from './rtdevbot';
55
const register = (app: App) => {
66
app.command('/sample-command', sampleCommandCallback);
77
app.command('/rtdevbot', botCommandHandler);
8+
app.command('/rtdevbot-dev', botCommandHandler);
89
};
910

1011
export default { register };

0 commit comments

Comments
 (0)