Skip to content

Commit

Permalink
Merge pull request #7 from theapexlab/birthday-interactions
Browse files Browse the repository at this point in the history
APEX-1587 - Birthday interactions
  • Loading branch information
BaDo2001 authored Nov 1, 2023
2 parents 13cc07e + 09306cf commit f6c36b1
Show file tree
Hide file tree
Showing 40 changed files with 976 additions and 185 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ VITE_SLACK_BOT_USER_ID=
VITE_CORE_SLACK_CHANNEL_ID=
# The slack dm id between the bot and you
VITE_SLACK_DM_ID=
# The id of the team
VITE_SLACK_TEAM_ID=
# The url of the local database
VITE_DB_URL=
# Used to indicate that the tests are running in a ci environment (not needed for local development)
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/pr-closed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Inject slug variables
uses: rlespinasse/github-slug-action@v4

- run: echo "STAGE=pr-${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}" >> $GITHUB_ENV
- run: echo "STAGE=pr-${{ github.event.number }}" >> $GITHUB_ENV

- name: Checkout repository
uses: actions/checkout@v3
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ jobs:
lint-build-and-test:
runs-on: ubuntu-latest
steps:
- name: Inject slug variables
uses: rlespinasse/github-slug-action@v4

- run: echo "STAGE=pr-${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}" >> $GITHUB_ENV
- run: echo "STAGE=pr-${{ github.event.number }}" >> $GITHUB_ENV

- name: Checkout repository
uses: actions/checkout@v3
Expand Down Expand Up @@ -103,6 +100,7 @@ jobs:
VITE_SLACK_BOT_USER_ID: ${{ secrets.SLACK_BOT_USER_ID_TEST }}
VITE_CORE_SLACK_CHANNEL_ID: ${{ secrets.CORE_SLACK_CHANNEL_ID_TEST }}
VITE_SLACK_DM_ID: ${{ secrets.SLACK_DM_ID_TEST }}
VITE_SLACK_TEAM_ID: ${{ secrets.SLACK_TEAM_ID_TEST }}
VITE_CI: true
VITE_DB_NAME: ${{ steps.sst-output.outputs.database }}
VITE_DB_SECRET_ARN: ${{ steps.sst-output.outputs.secretArn }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Extract stack outputs
id: sst-output
run: |
MIGRATION_FUNCTION=$(jq -r '.["${{env.STAGE}}-birthday-slack-bot-MyStack"].MigrationFunctionName' .sst/outputs.json)
MIGRATION_FUNCTION=$(jq -r '.["${{env.stage}}-birthday-slack-bot-MyStack"].MigrationFunctionName' .sst/outputs.json)
echo "migrationFunction=$MIGRATION_FUNCTION" >> "$GITHUB_OUTPUT"
- name: Migration
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Helps teams to find the best birthday gift for their colleagues.
- member_joined_channel
- member_left_channel

7. Open the `Interactivity & Shortcuts` sub-page -> enable interactivity. (We will add the url later.)

### The bot works with two channels:

- The core channel is the single source of truth regarding members who are part of the team.
Expand Down Expand Up @@ -69,8 +71,10 @@ npx sst secrets set RANDOM_SLACK_CHANNEL_ID <your-test-channel>

1. Find the ApiEndpoint url of your deployed app in the console output.
2. Open the `Event Subscriptions` sub-page.
3. Add the url: `<ApiEndpoint>/api/slack/callback` to the `Request URL` field.
3. Add the url: `<ApiEndpoint>/api/slack/event` to the `Request URL` field.
4. Slack sends a challenge request to the url to verify the endpoint. Make sure you have the app running locally for it to succeed.
5. Open the `Interactivity & Shortcuts` sub-page.
6. Add the url: `<ApiEndpoint>/api/slack/interaction` to the `Request URL` field.

## Run tests

Expand Down
181 changes: 161 additions & 20 deletions architecture/backend.drawio

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/core/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"idx": 0,
"version": "5",
"when": 1698670168682,
"tag": "0000_clammy_vargas",
"tag": "0000_initial_migration",
"breakpoints": true
}
]
}
}
24 changes: 24 additions & 0 deletions packages/core/db/saveBirthday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { users } from "./schema";
import { db } from ".";

type Args = {
user: string;
teamId: string;
birthday: string;
};

export const saveBirthday = async ({ birthday, teamId, user }: Args) => {
await db
.insert(users)
.values({
id: user,
teamId,
birthday: new Date(birthday),
})
.onConflictDoUpdate({
target: [users.id, users.teamId],
set: {
birthday: new Date(birthday),
},
});
};
26 changes: 19 additions & 7 deletions packages/core/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { z } from "zod";

const BaseEvent = z.object({
eventId: z.string().optional(),
});

const Events = z.object({
memberJoinedChannel: z.object({
memberJoinedChannel: BaseEvent.extend({
channel: z.string(),
user: z.string(),
eventId: z.string(),
}),
askBirthday: z.object({
askBirthday: BaseEvent.extend({
user: z.string(),
eventId: z.string(),
responseUrl: z.string().optional(),
}),
botJoined: z.object({
botJoined: BaseEvent.extend({
channel: z.string(),
eventId: z.string(),
}),
memberLeftChannel: z.object({
user: z.string(),
eventId: z.string(),
team: z.string(),
}),
birthdayFilled: z.object({
birthday: z.string(),
responseUrl: z.string(),
}),
birthdayConfirmed: z.object({
user: z.string(),
team: z.string(),
birthday: z.string(),
responseUrl: z.string(),
}),
});

Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
"dependencies": {
"@aws-sdk/client-rds-data": "^3.438.0",
"@slack/bolt": "^3.14.0",
"@slack/web-api": "^6.9.0",
"drizzle-orm": "^0.28.6",
"pg": "^8.11.3",
"zod": "^3.22.4"
}
}
}
63 changes: 63 additions & 0 deletions packages/core/services/slack/constructAskBirthdayMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { ChatPostMessageArguments } from "@slack/web-api";

import type { ChatReplaceMessageArguments } from "@/types/ChatReplaceMessageArguments";
import { pickBirthdayActionId } from "@/types/SlackInteractionRequest";

type Arguments = {
user: string;
name: string;
eventId?: string;
};

const constructBaseAskBirthdayMessage = ({
name,
eventId,
}: Arguments): Omit<ChatPostMessageArguments, "channel"> => ({
text: "Please share your birthday with us! 🥳",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `Hey ${name}! 👋`,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: "Please share your birthday with us! 🥳",
},
accessory: {
type: "datepicker",
initial_date: "1995-01-01",
placeholder: {
type: "plain_text",
text: "Select a date",
emoji: true,
},
action_id: pickBirthdayActionId,
},
},
],
metadata: {
event_type: "askBirthday",
event_payload: {
originalEventId: eventId,
},
},
});

export const constructAskBirthdayMessage = (
args: Arguments,
): ChatPostMessageArguments => ({
...constructBaseAskBirthdayMessage(args),
channel: args.user,
});

export const constructAskBirthdayMessageReplacement = (
args: Arguments,
): ChatReplaceMessageArguments => ({
...constructBaseAskBirthdayMessage(args),
replace_original: true,
});
16 changes: 16 additions & 0 deletions packages/core/services/slack/constructBirthdayConfirmedMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ChatReplaceMessageArguments } from "@/types/ChatReplaceMessageArguments";

export const constructBirthdayConfirmedMessage =
(): ChatReplaceMessageArguments => ({
replace_original: true,
text: "Thanks for submitting your birthday! 🎉",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `Thanks for submitting your birthday! 🎉`,
},
},
],
});
55 changes: 55 additions & 0 deletions packages/core/services/slack/constructConfirmBirthdayMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ChatReplaceMessageArguments } from "@/types/ChatReplaceMessageArguments";
import {
birthdayConfirmActionId,
birthdayIncorrectActionId,
} from "@/types/SlackInteractionRequest";

export const constructConfirmBirthdayMessage = (
birthday: string,
): ChatReplaceMessageArguments =>
({
replace_original: true,
text: "Confirm your birthday",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `Are you sure your birthday is ${birthday}?`,
},
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
emoji: true,
text: "Yes",
},
style: "primary",
action_id: birthdayConfirmActionId,
value: birthday,
},
{
type: "button",
text: {
type: "plain_text",
emoji: true,
text: "No",
},
style: "danger",
action_id: birthdayIncorrectActionId,
value: birthday,
},
],
},
],
metadata: {
event_type: "confirmBirthday",
event_payload: {
birthday,
},
},
}) satisfies ChatReplaceMessageArguments;
10 changes: 8 additions & 2 deletions packages/core/services/slack/createSlackApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import type { LogLevel } from "@slack/bolt";
import { App } from "@slack/bolt";
import { Config } from "sst/node/config";

let singletonApp: App | undefined;

export const createSlackApp = () => {
if (singletonApp) {
return singletonApp;
}

const token = Config.SLACK_BOT_TOKEN;
const signingSecret = Config.SLACK_SIGNING_SECRET;
const logLevel = Config.SLACK_LOG_LEVEL as LogLevel;
Expand All @@ -11,11 +17,11 @@ export const createSlackApp = () => {
throw new Error("Missing Slack config");
}

const app = new App({
singletonApp = new App({
signingSecret,
token,
logLevel,
});

return app;
return singletonApp;
};
8 changes: 8 additions & 0 deletions packages/core/types/ChatReplaceMessageArguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ChatPostMessageArguments } from "@slack/web-api";

export type ChatReplaceMessageArguments = Omit<
ChatPostMessageArguments,
"channel"
> & {
replace_original: boolean;
};
3 changes: 2 additions & 1 deletion packages/core/types/SlackEventRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as z from "zod";
import { z } from "zod";

export const SlackChallengeRequestSchema = z.object({
challenge: z.string(),
Expand All @@ -13,6 +13,7 @@ export const SlackEventSchema = z.object({
z.literal("member_left_channel"),
]),
user: z.string(),
team: z.string(),
channel: z.string(),
});

Expand Down
39 changes: 39 additions & 0 deletions packages/core/types/SlackInteractionRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { z } from "zod";

export const pickBirthdayActionId = "pickBirthday";
export const birthdayConfirmActionId = "birthdayConfirm";
export const birthdayIncorrectActionId = "birthdayIncorrect";

export const SlackInteractionRequestSchema = z.object({
type: z.literal("block_actions"),
user: z.object({
id: z.string(),
team_id: z.string(),
}),
actions: z
.array(
z.discriminatedUnion("type", [
z.object({
type: z.literal("datepicker"),
selected_date: z.string(),
action_id: z.literal(pickBirthdayActionId),
action_ts: z.string(),
}),
z.object({
type: z.literal("button"),
action_id: z.union([
z.literal(birthdayConfirmActionId),
z.literal(birthdayIncorrectActionId),
]),
action_ts: z.string(),
value: z.string(),
}),
]),
)
.length(1),
response_url: z.string(),
});

export type SlackInteractionRequest = z.infer<
typeof SlackInteractionRequestSchema
>;
Loading

0 comments on commit f6c36b1

Please sign in to comment.