Skip to content

Commit a189e8d

Browse files
committed
feat: leverage job queue for adding/removing Discord bot members
1 parent a4297a6 commit a189e8d

32 files changed

+1433
-232
lines changed

api-schema.graphql

+6
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ type Identity {
298298
id: String!
299299
name: String
300300
owner: User
301+
ownerId: String
301302
profile: JSON
302303
provider: IdentityProvider!
303304
providerId: String!
@@ -338,19 +339,23 @@ input LinkIdentityInput {
338339
}
339340

340341
type Log {
342+
bot: Bot
341343
botId: String
342344
communityId: String!
343345
createdAt: DateTime
344346
data: JSON
345347
id: String!
348+
identity: Identity
346349
identityProvider: IdentityProvider
347350
identityProviderId: String
348351
level: LogLevel!
349352
message: String!
350353
relatedId: String
351354
relatedType: LogRelatedType
355+
rule: Rule
352356
ruleId: String
353357
updatedAt: DateTime
358+
user: User
354359
userId: String
355360
}
356361

@@ -553,6 +558,7 @@ type Query {
553558
userFindOneLog(logId: String!): Log
554559
userFindOneRule(ruleId: String!): Rule
555560
userFindOneUser(username: String!): User
561+
userFindOneUserById(userId: String!): User
556562
userGetBotMembers(botId: String!, serverId: String!): [BotMember!]
557563
userGetBotRoles(botId: String!, serverId: String!): [DiscordRole!]
558564
userGetBotServer(botId: String!, serverId: String!): DiscordServer
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1+
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
2+
import { BullBoardModule } from '@bull-board/nestjs'
3+
import { BullModule } from '@nestjs/bullmq'
14
import { Module } from '@nestjs/common'
25
import { ApiCoreDataAccessModule } from '@pubkey-link/api-core-data-access'
3-
import { ApiBotService } from './api-bot.service'
46
import { ApiAdminBotService } from './api-admin-bot.service'
5-
import { ApiUserBotService } from './api-user-bot.service'
67
import { ApiBotManagerService } from './api-bot-manager.service'
8+
import { ApiBotMemberService } from './api-bot-member.service'
9+
import { ApiBotService } from './api-bot.service'
10+
import { ApiUserBotService } from './api-user-bot.service'
11+
import { API_BOT_MEMBER_ADD, API_BOT_MEMBER_REMOVE } from './helpers/api-bot.constants'
12+
import { ApiBotMemberAddProcessor } from './processors/api-bot-member-add-processor'
13+
import { ApiBotMemberRemoveProcessor } from './processors/api-bot-member-remove-processor'
14+
15+
const processors = [ApiBotMemberAddProcessor, ApiBotMemberRemoveProcessor]
716

817
@Module({
9-
imports: [ApiCoreDataAccessModule],
10-
providers: [ApiBotService, ApiAdminBotService, ApiBotManagerService, ApiUserBotService],
18+
imports: [
19+
ApiCoreDataAccessModule,
20+
BullModule.registerQueue({ name: API_BOT_MEMBER_ADD }),
21+
BullModule.registerQueue({ name: API_BOT_MEMBER_REMOVE }),
22+
BullBoardModule.forFeature({ name: API_BOT_MEMBER_ADD, adapter: BullMQAdapter }),
23+
BullBoardModule.forFeature({ name: API_BOT_MEMBER_REMOVE, adapter: BullMQAdapter }),
24+
],
25+
providers: [
26+
...processors,
27+
ApiAdminBotService,
28+
ApiBotManagerService,
29+
ApiBotMemberService,
30+
ApiBotService,
31+
ApiUserBotService,
32+
],
1133
exports: [ApiBotService],
1234
})
1335
export class ApiBotDataAccessModule {}

libs/api/bot/data-access/src/lib/api-bot-manager.service.ts

+23-34
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
22

33
import { createDiscordRestClient, DiscordBot } from '@pubkey-link/api-bot-util'
4-
import { ApiCoreService, IdentityProvider } from '@pubkey-link/api-core-data-access'
4+
import { ApiCoreService } from '@pubkey-link/api-core-data-access'
55

66
import { PermissionsString, User } from 'discord.js'
7+
import { ApiBotMemberService } from './api-bot-member.service'
78
import { BotStatus } from './entity/bot-status.enum'
89
import { DiscordRole, DiscordServer } from './entity/discord-server.entity'
910

1011
@Injectable()
1112
export class ApiBotManagerService implements OnModuleInit {
1213
private readonly logger = new Logger(ApiBotManagerService.name)
1314
private readonly bots = new Map<string, DiscordBot>()
14-
constructor(private readonly core: ApiCoreService) {}
15+
16+
constructor(private readonly core: ApiCoreService, private readonly botMember: ApiBotMemberService) {}
1517

1618
async onModuleInit() {
1719
const bots = await this.core.data.bot.findMany({ where: { status: BotStatus.Active } })
@@ -125,6 +127,7 @@ export class ApiBotManagerService implements OnModuleInit {
125127

126128
const instance = new DiscordBot({ botId, token: bot.token })
127129
await instance.start()
130+
await this.botMember.setupListeners(bot, instance)
128131
this.bots.set(bot.id, instance)
129132

130133
return true
@@ -167,11 +170,16 @@ export class ApiBotManagerService implements OnModuleInit {
167170
console.log(`Can't find bot.`, botId, serverId)
168171
return false
169172
}
173+
const community = await this.core.data.community.findFirst({ where: { bot: { id: botId } } })
174+
if (!community) {
175+
console.log(`Can't find community.`, botId, serverId)
176+
return false
177+
}
170178
this.logger.verbose(`Fetching members... ${botId} ${serverId}`)
171179

172180
const [discordIdentityIds, botMemberIds] = await Promise.all([
173-
this.getDiscordIdentityIds(),
174-
this.getBotMemberIds(botId, serverId),
181+
this.botMember.getDiscordIdentityIds(),
182+
this.botMember.getBotMemberIds(botId, serverId),
175183
])
176184
const members = await bot.getDiscordServerMembers(serverId)
177185

@@ -194,15 +202,19 @@ export class ApiBotManagerService implements OnModuleInit {
194202
for (const member of filtered) {
195203
const userId = member.id
196204
// const identityProviderId = discordIdentityIds.includes(member.id) ? member.id : undefined
197-
const created = await this.core.data.botMember.upsert({
198-
where: { botId_userId_serverId: { botId, userId, serverId } },
199-
update: {},
200-
create: {
205+
const created = await this.botMember.upsert({ botId, communityId: community.id, serverId, userId })
206+
if (!created) {
207+
this.logger.warn(`Failed to create bot member ${botId} ${serverId} ${userId}`)
208+
continue
209+
}
210+
211+
await this.core.logInfo(
212+
community.id,
213+
`Bot ${bot.client?.user?.username} added member ${member.user.username} to server ${serverId}`,
214+
{
201215
botId,
202-
serverId,
203-
userId,
204216
},
205-
})
217+
)
206218
this.logger.verbose(
207219
`${botId} ${serverId} Processed member ${created.id} ${member.user.username} (linked: ${!!member.id})`,
208220
)
@@ -215,29 +227,6 @@ export class ApiBotManagerService implements OnModuleInit {
215227
)
216228
return true
217229
}
218-
219-
private async getBotMemberIds(botId: string, serverId: string) {
220-
return this.core.data.botMember
221-
.findMany({ where: { botId, serverId } })
222-
.then((items) => items.map(({ userId }) => userId))
223-
}
224-
225-
private async getDiscordIdentityIds() {
226-
return this.core.data.identity
227-
.findMany({ where: { provider: IdentityProvider.Discord } })
228-
.then((items) => items.map((item) => item.providerId))
229-
}
230-
231-
async getBotMembers(botId: string, serverId: string) {
232-
return this.core.data.botMember.findMany({
233-
where: {
234-
botId,
235-
serverId,
236-
},
237-
include: { identity: { include: { owner: true } } },
238-
orderBy: { identity: { owner: { username: 'asc' } } },
239-
})
240-
}
241230
}
242231

243232
function convertPermissions(permissions: Record<PermissionsString, boolean>) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { InjectQueue } from '@nestjs/bullmq'
2+
import { Injectable, Logger } from '@nestjs/common'
3+
import { Bot } from '@prisma/client'
4+
5+
import { DiscordBot } from '@pubkey-link/api-bot-util'
6+
import { ApiCoreService, IdentityProvider } from '@pubkey-link/api-core-data-access'
7+
import { Queue } from 'bullmq'
8+
import { API_BOT_MEMBER_ADD, API_BOT_MEMBER_REMOVE } from './helpers/api-bot.constants'
9+
10+
@Injectable()
11+
export class ApiBotMemberService {
12+
private readonly logger = new Logger(ApiBotMemberService.name)
13+
14+
constructor(
15+
@InjectQueue(API_BOT_MEMBER_ADD) private botMemberAddQueue: Queue,
16+
@InjectQueue(API_BOT_MEMBER_REMOVE) private botMemberRemoveQueue: Queue,
17+
private readonly core: ApiCoreService,
18+
) {}
19+
20+
async setupListeners(bot: Bot, instance: DiscordBot) {
21+
if (!instance.client?.user) {
22+
this.logger.warn(`Bot client on instance not found.`)
23+
return
24+
}
25+
this.logger.verbose(`Setting up listeners for bot ${bot.name}`)
26+
instance.client?.on('guildMemberAdd', (member) => this.scheduleAddMember(bot, member.guild.id, member.id))
27+
instance.client.on('guildMemberRemove', (member) => this.scheduleRemoveMember(bot, member.guild.id, member.id))
28+
}
29+
30+
async upsert({
31+
botId,
32+
communityId,
33+
serverId,
34+
userId,
35+
}: {
36+
botId: string
37+
communityId: string
38+
serverId: string
39+
userId: string
40+
}) {
41+
const identity = await this.core.findUserByIdentity({
42+
provider: IdentityProvider.Discord,
43+
providerId: userId,
44+
})
45+
if (!identity) {
46+
await this.core.logError(communityId, `User ${userId} joined ${serverId} but identity not found`, {
47+
botId,
48+
identityProvider: IdentityProvider.Discord,
49+
identityProviderId: userId,
50+
})
51+
return
52+
}
53+
54+
return this.core.data.botMember
55+
.upsert({
56+
where: { botId_userId_serverId: { botId, userId, serverId } },
57+
update: { botId, serverId, userId },
58+
create: { botId, serverId, userId },
59+
})
60+
.then(async (created) => {
61+
await this.core.logInfo(communityId, `Added ${userId} to ${serverId}`, {
62+
botId,
63+
identityProvider: IdentityProvider.Discord,
64+
identityProviderId: userId,
65+
})
66+
return created
67+
})
68+
}
69+
async remove({
70+
botId,
71+
communityId,
72+
serverId,
73+
userId,
74+
}: {
75+
botId: string
76+
communityId: string
77+
serverId: string
78+
userId: string
79+
}) {
80+
return this.core.data.botMember
81+
.delete({ where: { botId_userId_serverId: { botId, userId, serverId } } })
82+
.then((deleted) => {
83+
this.core.logInfo(communityId, `Removed ${userId} from ${serverId}`, {
84+
botId,
85+
identityProvider: IdentityProvider.Discord,
86+
identityProviderId: userId,
87+
})
88+
return deleted
89+
})
90+
}
91+
92+
async getBotMemberIds(botId: string, serverId: string) {
93+
return this.core.data.botMember
94+
.findMany({ where: { botId, serverId } })
95+
.then((items) => items.map(({ userId }) => userId))
96+
}
97+
98+
async getDiscordIdentityIds() {
99+
return this.core.data.identity
100+
.findMany({ where: { provider: IdentityProvider.Discord } })
101+
.then((items) => items.map((item) => item.providerId))
102+
}
103+
104+
async getBotMembers(botId: string, serverId: string) {
105+
return this.core.data.botMember.findMany({
106+
where: {
107+
botId,
108+
serverId,
109+
},
110+
include: { identity: { include: { owner: true } } },
111+
orderBy: { identity: { owner: { username: 'asc' } } },
112+
})
113+
}
114+
115+
private async scheduleAddMember(bot: Bot, serverId: string, userId: string) {
116+
const jobId = `${bot.id}-${serverId}-${userId}`
117+
await this.botMemberAddQueue
118+
.add('member-add', { botId: bot.id, communityId: bot.communityId, serverId, userId }, { jobId })
119+
.then((res) => {
120+
this.logger.verbose(`scheduleAddMember queued: ${res.id}`)
121+
})
122+
.catch((err) => {
123+
this.logger.error(`scheduleAddMember error: ${jobId}: ${err}`)
124+
})
125+
}
126+
private async scheduleRemoveMember(bot: Bot, serverId: string, userId: string) {
127+
const jobId = `${bot.id}-${serverId}-${userId}`
128+
await this.botMemberRemoveQueue
129+
.add('member-remove', { botId: bot.id, communityId: bot.communityId, serverId, userId }, { jobId })
130+
.then((res) => {
131+
this.logger.verbose(`scheduleRemoveMember queued: ${res.id}`)
132+
})
133+
.catch((err) => {
134+
this.logger.error(`scheduleRemoveMember error: ${jobId}: ${err}`)
135+
})
136+
}
137+
}

libs/api/bot/data-access/src/lib/api-bot.service.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Injectable } from '@nestjs/common'
22
import { ApiAdminBotService } from './api-admin-bot.service'
3-
import { ApiUserBotService } from './api-user-bot.service'
43
import { ApiBotManagerService } from './api-bot-manager.service'
4+
import { ApiBotMemberService } from './api-bot-member.service'
5+
import { ApiUserBotService } from './api-user-bot.service'
56

67
@Injectable()
78
export class ApiBotService {
89
constructor(
910
readonly manager: ApiBotManagerService,
11+
readonly member: ApiBotMemberService,
1012
readonly admin: ApiAdminBotService,
1113
readonly user: ApiUserBotService,
1214
) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const API_BOT_MEMBER_ADD = 'api-bot-member-add'
2+
export const API_BOT_MEMBER_REMOVE = 'api-bot-member-remove'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Processor, WorkerHost } from '@nestjs/bullmq'
2+
import { Logger } from '@nestjs/common'
3+
import { BotMember } from '@prisma/client'
4+
import { ApiCoreService } from '@pubkey-link/api-core-data-access'
5+
import { IdentityProvider } from '@pubkey-link/sdk'
6+
import { Job } from 'bullmq'
7+
import { ApiBotMemberService } from '../api-bot-member.service'
8+
import { API_BOT_MEMBER_ADD } from '../helpers/api-bot.constants'
9+
10+
export interface ApiBotMemberAddPayload {
11+
botId: string
12+
communityId: string
13+
serverId: string
14+
userId: string
15+
}
16+
17+
@Processor(API_BOT_MEMBER_ADD)
18+
export class ApiBotMemberAddProcessor extends WorkerHost {
19+
private readonly logger = new Logger(ApiBotMemberAddProcessor.name)
20+
constructor(private readonly core: ApiCoreService, private readonly member: ApiBotMemberService) {
21+
super()
22+
}
23+
24+
override async process(
25+
job: Job<ApiBotMemberAddPayload, BotMember | undefined, string>,
26+
): Promise<BotMember | undefined> {
27+
await job.updateProgress(0)
28+
const added = await this.member.upsert(job.data)
29+
if (added) {
30+
await job.log(`Added ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`)
31+
await this.core.logInfo(
32+
job.data.communityId,
33+
`Added ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`,
34+
{ botId: job.data.botId, identityProvider: IdentityProvider.Discord, identityProviderId: job.data.userId },
35+
)
36+
return added
37+
} else {
38+
await job.log(`Failed to add ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`)
39+
await this.core.logError(
40+
job.data.communityId,
41+
`Failed to add ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`,
42+
{ botId: job.data.botId, identityProvider: IdentityProvider.Discord, identityProviderId: job.data.userId },
43+
)
44+
return undefined
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)