Skip to content

Commit 4c3c2e9

Browse files
authored
add /notifications command (#255)
1 parent 583f77f commit 4c3c2e9

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
grafana:
3-
image: grafana/otel-lgtm:0.8.6
3+
image: grafana/otel-lgtm:0.11.5
44
container_name: grafana
55
restart: unless-stopped
66
ports:
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { DiscordGatewayLayer } from "@chat/discord/DiscordGateway"
2+
import assert from "assert"
3+
import { Discord, DiscordREST, Ix, UI } from "dfx"
4+
import { InteractionsRegistry } from "dfx/gateway"
5+
import { Array, Effect, Layer } from "effect"
6+
import { RolesCache } from "./RolesCache.ts"
7+
8+
export const NotificationsLayer = Effect.gen(function*() {
9+
const ix = yield* InteractionsRegistry
10+
const rolesCache = yield* RolesCache
11+
const rest = yield* DiscordREST
12+
13+
const notificationRoles = Effect.fnUntraced(function*(guildId: string) {
14+
const roles = yield* rolesCache.getForParent(guildId)
15+
return Array.fromIterable(roles.values()).filter((role) =>
16+
role.name.startsWith("🔔 ")
17+
)
18+
})
19+
20+
const message = Effect.fn("Notifications.message")(
21+
function*(ix: Discord.APIInteraction, userRoles?: Array<string>) {
22+
const guildId = ix.guild_id
23+
if (!guildId) {
24+
return UI.components([
25+
UI.textDisplay("This command can only be used in a server.")
26+
], { ephemeral: true })
27+
}
28+
29+
const roles = yield* notificationRoles(guildId)
30+
if (roles.length === 0) {
31+
return UI.components([
32+
UI.textDisplay("No notification roles found in this server.")
33+
], { ephemeral: true })
34+
}
35+
36+
userRoles = userRoles ?? ix.member!.roles
37+
38+
return UI.components([
39+
UI.textDisplay("Select the notifications you want to receive:"),
40+
UI.row([
41+
UI.select({
42+
custom_id: "notifications_role",
43+
placeholder: "No notifications selected",
44+
min_values: 0,
45+
max_values: roles.length,
46+
options: roles.map((role) => ({
47+
label: role.name,
48+
value: role.id,
49+
default: userRoles.includes(role.id)
50+
}))
51+
})
52+
])
53+
], { ephemeral: true })
54+
}
55+
)
56+
57+
const command = Ix.guild(
58+
{
59+
name: "notifications",
60+
description: "Choose which notifications you want to receive"
61+
},
62+
Effect.fn("Notifications.command")(function*(ix) {
63+
return Ix.response({
64+
type: 4,
65+
data: yield* message(ix.interaction)
66+
})
67+
})
68+
)
69+
70+
const select = Ix.messageComponent(
71+
Ix.id("notifications_role"),
72+
Effect.gen(function*() {
73+
const ix = yield* Ix.Interaction
74+
const data = yield* Ix.MessageComponentData
75+
assert(data.component_type === 3)
76+
const roles = yield* notificationRoles(ix.guild_id!)
77+
78+
const userRoles = new Set(ix.member?.roles ?? [])
79+
for (const role of roles) {
80+
const currentlyHas = userRoles.has(role.id)
81+
const shouldHave = data.values.includes(role.id)
82+
if (currentlyHas === shouldHave) {
83+
continue
84+
} else if (shouldHave) {
85+
userRoles.add(role.id)
86+
yield* rest.addGuildMemberRole(
87+
ix.guild_id!,
88+
ix.member!.user.id,
89+
role.id
90+
)
91+
} else {
92+
userRoles.delete(role.id)
93+
yield* rest.deleteGuildMemberRole(
94+
ix.guild_id!,
95+
ix.member!.user.id,
96+
role.id
97+
)
98+
}
99+
}
100+
return Ix.response({
101+
type: Discord.InteractionCallbackTypes.UPDATE_MESSAGE,
102+
data: yield* message(ix, Array.fromIterable(userRoles))
103+
})
104+
})
105+
)
106+
107+
yield* ix.register(
108+
Ix.builder.add(command).add(select).catchAll(Effect.logError)
109+
)
110+
}).pipe(
111+
Layer.scopedDiscard,
112+
Layer.provide([RolesCache.Default, DiscordGatewayLayer])
113+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DiscordGatewayLayer } from "@chat/discord/DiscordGateway"
2+
import { Cache } from "dfx"
3+
import { CachePrelude } from "dfx/gateway"
4+
import { Effect } from "effect"
5+
6+
export class RolesCache extends Effect.Service<RolesCache>()(
7+
"app/RolesCache",
8+
{
9+
scoped: CachePrelude.roles(Cache.memoryParentDriver()),
10+
dependencies: [DiscordGatewayLayer]
11+
}
12+
) {}

packages/discord-bot/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DocsLookupLive } from "./DocsLookup.ts"
77
import { IssueifierLive } from "./Issueifier.ts"
88
import { MessageLoggerLayer } from "./MessageLogger.ts"
99
import { NoEmbedLive } from "./NoEmbed.ts"
10+
import { NotificationsLayer } from "./Notifications.ts"
1011
import { PlaygroundLive } from "./Playground.ts"
1112
import { RemindersLive } from "./Reminders.ts"
1213
import { ReproRequesterLive } from "./ReproRequester.ts"
@@ -27,6 +28,7 @@ const MainLive = Layer.mergeAll(
2728
DocsLookupLive,
2829
IssueifierLive,
2930
MessageLoggerLayer,
31+
NotificationsLayer,
3032
PlaygroundLive,
3133
RemindersLive,
3234
ReproRequesterLive,

0 commit comments

Comments
 (0)