-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: leverage job queue for adding/removing Discord bot members
- Loading branch information
Showing
32 changed files
with
1,433 additions
and
232 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 26 additions & 4 deletions
30
libs/api/bot/data-access/src/lib/api-bot-data-access.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,35 @@ | ||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter' | ||
import { BullBoardModule } from '@bull-board/nestjs' | ||
import { BullModule } from '@nestjs/bullmq' | ||
import { Module } from '@nestjs/common' | ||
import { ApiCoreDataAccessModule } from '@pubkey-link/api-core-data-access' | ||
import { ApiBotService } from './api-bot.service' | ||
import { ApiAdminBotService } from './api-admin-bot.service' | ||
import { ApiUserBotService } from './api-user-bot.service' | ||
import { ApiBotManagerService } from './api-bot-manager.service' | ||
import { ApiBotMemberService } from './api-bot-member.service' | ||
import { ApiBotService } from './api-bot.service' | ||
import { ApiUserBotService } from './api-user-bot.service' | ||
import { API_BOT_MEMBER_ADD, API_BOT_MEMBER_REMOVE } from './helpers/api-bot.constants' | ||
import { ApiBotMemberAddProcessor } from './processors/api-bot-member-add-processor' | ||
import { ApiBotMemberRemoveProcessor } from './processors/api-bot-member-remove-processor' | ||
|
||
const processors = [ApiBotMemberAddProcessor, ApiBotMemberRemoveProcessor] | ||
|
||
@Module({ | ||
imports: [ApiCoreDataAccessModule], | ||
providers: [ApiBotService, ApiAdminBotService, ApiBotManagerService, ApiUserBotService], | ||
imports: [ | ||
ApiCoreDataAccessModule, | ||
BullModule.registerQueue({ name: API_BOT_MEMBER_ADD }), | ||
BullModule.registerQueue({ name: API_BOT_MEMBER_REMOVE }), | ||
BullBoardModule.forFeature({ name: API_BOT_MEMBER_ADD, adapter: BullMQAdapter }), | ||
BullBoardModule.forFeature({ name: API_BOT_MEMBER_REMOVE, adapter: BullMQAdapter }), | ||
], | ||
providers: [ | ||
...processors, | ||
ApiAdminBotService, | ||
ApiBotManagerService, | ||
ApiBotMemberService, | ||
ApiBotService, | ||
ApiUserBotService, | ||
], | ||
exports: [ApiBotService], | ||
}) | ||
export class ApiBotDataAccessModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
libs/api/bot/data-access/src/lib/api-bot-member.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { InjectQueue } from '@nestjs/bullmq' | ||
import { Injectable, Logger } from '@nestjs/common' | ||
import { Bot } from '@prisma/client' | ||
|
||
import { DiscordBot } from '@pubkey-link/api-bot-util' | ||
import { ApiCoreService, IdentityProvider } from '@pubkey-link/api-core-data-access' | ||
import { Queue } from 'bullmq' | ||
import { API_BOT_MEMBER_ADD, API_BOT_MEMBER_REMOVE } from './helpers/api-bot.constants' | ||
|
||
@Injectable() | ||
export class ApiBotMemberService { | ||
private readonly logger = new Logger(ApiBotMemberService.name) | ||
|
||
constructor( | ||
@InjectQueue(API_BOT_MEMBER_ADD) private botMemberAddQueue: Queue, | ||
@InjectQueue(API_BOT_MEMBER_REMOVE) private botMemberRemoveQueue: Queue, | ||
private readonly core: ApiCoreService, | ||
) {} | ||
|
||
async setupListeners(bot: Bot, instance: DiscordBot) { | ||
if (!instance.client?.user) { | ||
this.logger.warn(`Bot client on instance not found.`) | ||
return | ||
} | ||
this.logger.verbose(`Setting up listeners for bot ${bot.name}`) | ||
instance.client?.on('guildMemberAdd', (member) => this.scheduleAddMember(bot, member.guild.id, member.id)) | ||
instance.client.on('guildMemberRemove', (member) => this.scheduleRemoveMember(bot, member.guild.id, member.id)) | ||
} | ||
|
||
async upsert({ | ||
botId, | ||
communityId, | ||
serverId, | ||
userId, | ||
}: { | ||
botId: string | ||
communityId: string | ||
serverId: string | ||
userId: string | ||
}) { | ||
const identity = await this.core.findUserByIdentity({ | ||
provider: IdentityProvider.Discord, | ||
providerId: userId, | ||
}) | ||
if (!identity) { | ||
await this.core.logError(communityId, `User ${userId} joined ${serverId} but identity not found`, { | ||
botId, | ||
identityProvider: IdentityProvider.Discord, | ||
identityProviderId: userId, | ||
}) | ||
return | ||
} | ||
|
||
return this.core.data.botMember | ||
.upsert({ | ||
where: { botId_userId_serverId: { botId, userId, serverId } }, | ||
update: { botId, serverId, userId }, | ||
create: { botId, serverId, userId }, | ||
}) | ||
.then(async (created) => { | ||
await this.core.logInfo(communityId, `Added ${userId} to ${serverId}`, { | ||
botId, | ||
identityProvider: IdentityProvider.Discord, | ||
identityProviderId: userId, | ||
}) | ||
return created | ||
}) | ||
} | ||
async remove({ | ||
botId, | ||
communityId, | ||
serverId, | ||
userId, | ||
}: { | ||
botId: string | ||
communityId: string | ||
serverId: string | ||
userId: string | ||
}) { | ||
return this.core.data.botMember | ||
.delete({ where: { botId_userId_serverId: { botId, userId, serverId } } }) | ||
.then((deleted) => { | ||
this.core.logInfo(communityId, `Removed ${userId} from ${serverId}`, { | ||
botId, | ||
identityProvider: IdentityProvider.Discord, | ||
identityProviderId: userId, | ||
}) | ||
return deleted | ||
}) | ||
} | ||
|
||
async getBotMemberIds(botId: string, serverId: string) { | ||
return this.core.data.botMember | ||
.findMany({ where: { botId, serverId } }) | ||
.then((items) => items.map(({ userId }) => userId)) | ||
} | ||
|
||
async getDiscordIdentityIds() { | ||
return this.core.data.identity | ||
.findMany({ where: { provider: IdentityProvider.Discord } }) | ||
.then((items) => items.map((item) => item.providerId)) | ||
} | ||
|
||
async getBotMembers(botId: string, serverId: string) { | ||
return this.core.data.botMember.findMany({ | ||
where: { | ||
botId, | ||
serverId, | ||
}, | ||
include: { identity: { include: { owner: true } } }, | ||
orderBy: { identity: { owner: { username: 'asc' } } }, | ||
}) | ||
} | ||
|
||
private async scheduleAddMember(bot: Bot, serverId: string, userId: string) { | ||
const jobId = `${bot.id}-${serverId}-${userId}` | ||
await this.botMemberAddQueue | ||
.add('member-add', { botId: bot.id, communityId: bot.communityId, serverId, userId }, { jobId }) | ||
.then((res) => { | ||
this.logger.verbose(`scheduleAddMember queued: ${res.id}`) | ||
}) | ||
.catch((err) => { | ||
this.logger.error(`scheduleAddMember error: ${jobId}: ${err}`) | ||
}) | ||
} | ||
private async scheduleRemoveMember(bot: Bot, serverId: string, userId: string) { | ||
const jobId = `${bot.id}-${serverId}-${userId}` | ||
await this.botMemberRemoveQueue | ||
.add('member-remove', { botId: bot.id, communityId: bot.communityId, serverId, userId }, { jobId }) | ||
.then((res) => { | ||
this.logger.verbose(`scheduleRemoveMember queued: ${res.id}`) | ||
}) | ||
.catch((err) => { | ||
this.logger.error(`scheduleRemoveMember error: ${jobId}: ${err}`) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
libs/api/bot/data-access/src/lib/helpers/api-bot.constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const API_BOT_MEMBER_ADD = 'api-bot-member-add' | ||
export const API_BOT_MEMBER_REMOVE = 'api-bot-member-remove' |
47 changes: 47 additions & 0 deletions
47
libs/api/bot/data-access/src/lib/processors/api-bot-member-add-processor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Processor, WorkerHost } from '@nestjs/bullmq' | ||
import { Logger } from '@nestjs/common' | ||
import { BotMember } from '@prisma/client' | ||
import { ApiCoreService } from '@pubkey-link/api-core-data-access' | ||
import { IdentityProvider } from '@pubkey-link/sdk' | ||
import { Job } from 'bullmq' | ||
import { ApiBotMemberService } from '../api-bot-member.service' | ||
import { API_BOT_MEMBER_ADD } from '../helpers/api-bot.constants' | ||
|
||
export interface ApiBotMemberAddPayload { | ||
botId: string | ||
communityId: string | ||
serverId: string | ||
userId: string | ||
} | ||
|
||
@Processor(API_BOT_MEMBER_ADD) | ||
export class ApiBotMemberAddProcessor extends WorkerHost { | ||
private readonly logger = new Logger(ApiBotMemberAddProcessor.name) | ||
constructor(private readonly core: ApiCoreService, private readonly member: ApiBotMemberService) { | ||
super() | ||
} | ||
|
||
override async process( | ||
job: Job<ApiBotMemberAddPayload, BotMember | undefined, string>, | ||
): Promise<BotMember | undefined> { | ||
await job.updateProgress(0) | ||
const added = await this.member.upsert(job.data) | ||
if (added) { | ||
await job.log(`Added ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`) | ||
await this.core.logInfo( | ||
job.data.communityId, | ||
`Added ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`, | ||
{ botId: job.data.botId, identityProvider: IdentityProvider.Discord, identityProviderId: job.data.userId }, | ||
) | ||
return added | ||
} else { | ||
await job.log(`Failed to add ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`) | ||
await this.core.logError( | ||
job.data.communityId, | ||
`Failed to add ${job.data.userId} to ${job.data.serverId} by bot ${job.data.botId}`, | ||
{ botId: job.data.botId, identityProvider: IdentityProvider.Discord, identityProviderId: job.data.userId }, | ||
) | ||
return undefined | ||
} | ||
} | ||
} |
Oops, something went wrong.