Skip to content

Commit e0100c1

Browse files
committed
heywaffle, slack event usecase 정리
1 parent cb1c05b commit e0100c1

File tree

5 files changed

+188
-160
lines changed

5 files changed

+188
-160
lines changed

src/infrastructures/implementMongoAtlasWaffleRepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import type { MongoClient } from 'mongodb';
22
import { z } from 'zod';
33
import type { SlackID } from '../entities/Slack';
44
import type { implementDashboardService } from '../services/DashboardService';
5-
import type { implementSlackEventService } from '../services/SlackEventService';
5+
import type { getSlackWatcherUsecsae } from '../services/SlackEventService';
66
import type { implementWaffleService } from '../services/WaffleService';
77

88
export const implementMongoAtlasWaffleRepository = ({
99
mongoClient,
1010
}: { mongoClient: Pick<MongoClient, 'db'> }): Parameters<
11-
typeof implementSlackEventService
11+
typeof getSlackWatcherUsecsae
1212
>[0]['waffleRepository'] &
1313
Parameters<typeof implementDashboardService>[0]['waffleRepository'] &
1414
Parameters<typeof implementWaffleService>[0]['waffleRepository'] => {

src/main.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { implementMemberWaffleDotComRepository } from './infrastructures/impleme
66
import { implementMongoAtlasWaffleRepository } from './infrastructures/implementMongoAtlasWaffleRepository';
77
import { implementSlackPresenter } from './infrastructures/implementSlackPresenter';
88
import { implementDeploymentService } from './services/GithubDeploymentService';
9-
import { implementSlackEventService } from './services/SlackEventService';
9+
1010
import { implementWaffleService } from './services/WaffleService';
1111

1212
import type { MongoClient } from 'mongodb';
@@ -15,6 +15,8 @@ import {
1515
type WaffleGraphDashboardService,
1616
implementWaffleGraphDashboardService,
1717
} from './services/WaffleGraphDashboardService';
18+
import { getHeywaffleUsecase } from './usecases/HeywaffleUsecase';
19+
import { getSlackWatcherUsecsae } from './usecases/SlackWatcherUsecase';
1820

1921
export const handle = async (
2022
dependencies: {
@@ -33,11 +35,13 @@ export const handle = async (
3335
},
3436
request: Request,
3537
): Promise<Response> => {
36-
const slackService = implementSlackEventService({
38+
const slackWatcherUsecase = getSlackWatcherUsecsae({
3739
messengerPresenter: implementSlackPresenter({
3840
slackClient: dependencies.slackClient,
3941
channelId: env.slackWatcherChannelId,
4042
}),
43+
});
44+
const heywaffleUsecase = getHeywaffleUsecase({
4145
waffleRepository: implementMongoAtlasWaffleRepository(dependencies),
4246
messageRepository: {
4347
getPermalink: async ({ channel, ts }) =>
@@ -98,9 +102,16 @@ export const handle = async (
98102

99103
if (body.type === 'url_verification') return new Response(body.challenge, { status: 200 });
100104

101-
// 3초 안에 응답하지 않으면 웹훅이 다시 들어오므로 await 하지 않고 바로 응답한다
102-
slackService.handleEvent(body.event);
105+
if (body.event.type === 'message') heywaffleUsecase.handleSlackMessage(body.event);
106+
if (
107+
body.event.type === 'channel_created' ||
108+
body.event.type === 'channel_archive' ||
109+
body.event.type === 'channel_unarchive' ||
110+
body.event.type === 'channel_rename'
111+
)
112+
slackWatcherUsecase.handleEvent(body.event);
103113

114+
// 3초 안에 응답하지 않으면 웹훅이 다시 들어오므로 await 하지 않는다
104115
return new Response(null, { status: 204 });
105116
}
106117

src/services/SlackEventService.ts

Lines changed: 0 additions & 154 deletions
This file was deleted.

src/usecases/HeywaffleUsecase.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { AnyBlock, MessageEvent } from '@slack/web-api';
2+
import type { SlackID } from '../entities/Slack';
3+
import type { Log } from '../entities/Waffle';
4+
import { getTodayStartAsKST } from '../utils/getTodayStartAsKST';
5+
6+
type HeywaffleUsecase = {
7+
handleSlackMessage: (event: MessageEvent) => Promise<void>;
8+
};
9+
10+
type Mention = `<@${SlackID}>`;
11+
const slackIDToMention = (id: SlackID): Mention => `<@${id}>`;
12+
const mentionToSlackID = (mention: Mention): SlackID => mention.slice(2, -1) as SlackID;
13+
14+
export const getHeywaffleUsecase = ({
15+
messageRepository,
16+
waffleRepository,
17+
}: {
18+
messageRepository: {
19+
getPermalink: (_: { channel: string; ts: string }) => Promise<{ link: string }>;
20+
sendMessage: (_: { channel: string; text: string; blocks?: AnyBlock[] }) => Promise<void>;
21+
};
22+
waffleRepository: {
23+
insert: (_: Log[]) => Promise<void>;
24+
listLogs: (_: { from: Date; to: Date }) => Promise<{ logs: Log[] }>;
25+
};
26+
}): HeywaffleUsecase => {
27+
return {
28+
handleSlackMessage: async (event) => {
29+
if (!('user' in event && typeof event.user === 'string' && event.subtype === undefined))
30+
return;
31+
const user = event.user as SlackID;
32+
const targetUsers = ((event.text?.match(/<@[A-Z0-9]+>/g) ?? []) as Mention[]).filter(
33+
(m) => m !== slackIDToMention(user),
34+
);
35+
36+
if (targetUsers.length === 0) return;
37+
38+
const todayGivenCount = (
39+
await waffleRepository.listLogs({
40+
from: getTodayStartAsKST(new Date()),
41+
to: new Date(),
42+
})
43+
).logs
44+
.filter((l) => l.from === user)
45+
.reduce((a, c) => a + c.count, 0);
46+
const dayMax = 5;
47+
const left = dayMax - todayGivenCount;
48+
49+
const count = event.text?.match(/:waffle:/g)?.length ?? 0;
50+
51+
if (count === 0) return;
52+
const total = count * targetUsers.length;
53+
54+
const href = (
55+
await messageRepository.getPermalink({
56+
channel: event.channel,
57+
ts: event.ts,
58+
})
59+
).link;
60+
61+
if (left < total) {
62+
await messageRepository.sendMessage({
63+
channel: event.user,
64+
text: `*You have only ${left} ${left === 1 ? 'Waffle' : 'Waffles'} left for today!*`,
65+
blocks: [
66+
{
67+
type: 'section',
68+
text: {
69+
type: 'mrkdwn',
70+
text: `*You have only ${left} ${left === 1 ? 'Waffle' : 'Waffles'} left for today!*`,
71+
},
72+
accessory: {
73+
type: 'button',
74+
text: { type: 'plain_text', text: 'View Message' },
75+
url: href,
76+
},
77+
},
78+
],
79+
});
80+
return;
81+
}
82+
83+
await Promise.all([
84+
...[
85+
{
86+
channel: event.user,
87+
text: `*You Gave ${count} ${count === 1 ? 'Waffle' : 'Waffles'} to ${targetUsers.join(',')} (${left - total} left)*`,
88+
},
89+
...targetUsers.map((u) => ({
90+
channel: mentionToSlackID(u),
91+
text: `*You Received ${count} ${count === 1 ? 'Waffle' : 'Waffles'} from ${slackIDToMention(user)}!*`,
92+
})),
93+
].map(({ channel, text }) =>
94+
messageRepository.sendMessage({
95+
channel,
96+
text,
97+
blocks: [
98+
{
99+
type: 'section',
100+
text: { type: 'mrkdwn', text },
101+
accessory: {
102+
type: 'button',
103+
text: { type: 'plain_text', text: 'View Message' },
104+
url: href,
105+
},
106+
},
107+
],
108+
}),
109+
),
110+
111+
waffleRepository.insert(
112+
targetUsers.map((targetUser) => ({
113+
from: user,
114+
to: mentionToSlackID(targetUser),
115+
count,
116+
href,
117+
date: new Date(),
118+
})),
119+
),
120+
]);
121+
},
122+
};
123+
};

src/usecases/SlackWatcherUsecase.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type {
2+
ChannelArchiveEvent,
3+
ChannelCreatedEvent,
4+
ChannelRenameEvent,
5+
ChannelUnarchiveEvent,
6+
} from '@slack/web-api';
7+
import type { MessengerPresenter } from '../presenters/MessengerPresenter';
8+
9+
type SlackWatcherUsecase = {
10+
handleEvent: (
11+
event: ChannelArchiveEvent | ChannelCreatedEvent | ChannelRenameEvent | ChannelUnarchiveEvent,
12+
) => Promise<void>;
13+
};
14+
15+
export const getSlackWatcherUsecsae = ({
16+
messengerPresenter,
17+
}: {
18+
messengerPresenter: MessengerPresenter;
19+
}): SlackWatcherUsecase => {
20+
return {
21+
handleEvent: async (event) => {
22+
switch (event.type) {
23+
case 'channel_archive':
24+
await messengerPresenter.sendMessage(({ formatChannel }) => ({
25+
text: `${formatChannel(event.channel)} 채널이 보관되었어요`,
26+
}));
27+
break;
28+
case 'channel_created':
29+
await messengerPresenter.sendMessage(({ formatChannel }) => ({
30+
text: `${formatChannel(event.channel.id)} 채널이 생성되었어요`,
31+
}));
32+
break;
33+
case 'channel_rename':
34+
await messengerPresenter.sendMessage(({ formatChannel }) => ({
35+
text: `${formatChannel(event.channel.id)} 채널 이름이 변경되었어요`,
36+
}));
37+
break;
38+
case 'channel_unarchive':
39+
await messengerPresenter.sendMessage(({ formatChannel }) => ({
40+
text: `${formatChannel(event.channel)} 채널이 보관 취소되었어요`,
41+
}));
42+
break;
43+
default:
44+
throw new Error('unexpected event');
45+
}
46+
},
47+
};
48+
};

0 commit comments

Comments
 (0)