Skip to content

Commit

Permalink
feat: leverage job queue for adding/removing Discord bot members
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Feb 8, 2024
1 parent a4297a6 commit a189e8d
Show file tree
Hide file tree
Showing 32 changed files with 1,433 additions and 232 deletions.
6 changes: 6 additions & 0 deletions api-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ type Identity {
id: String!
name: String
owner: User
ownerId: String
profile: JSON
provider: IdentityProvider!
providerId: String!
Expand Down Expand Up @@ -338,19 +339,23 @@ input LinkIdentityInput {
}

type Log {
bot: Bot
botId: String
communityId: String!
createdAt: DateTime
data: JSON
id: String!
identity: Identity
identityProvider: IdentityProvider
identityProviderId: String
level: LogLevel!
message: String!
relatedId: String
relatedType: LogRelatedType
rule: Rule
ruleId: String
updatedAt: DateTime
user: User
userId: String
}

Expand Down Expand Up @@ -553,6 +558,7 @@ type Query {
userFindOneLog(logId: String!): Log
userFindOneRule(ruleId: String!): Rule
userFindOneUser(username: String!): User
userFindOneUserById(userId: String!): User
userGetBotMembers(botId: String!, serverId: String!): [BotMember!]
userGetBotRoles(botId: String!, serverId: String!): [DiscordRole!]
userGetBotServer(botId: String!, serverId: String!): DiscordServer
Expand Down
30 changes: 26 additions & 4 deletions libs/api/bot/data-access/src/lib/api-bot-data-access.module.ts
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 {}
57 changes: 23 additions & 34 deletions libs/api/bot/data-access/src/lib/api-bot-manager.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'

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

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

@Injectable()
export class ApiBotManagerService implements OnModuleInit {
private readonly logger = new Logger(ApiBotManagerService.name)
private readonly bots = new Map<string, DiscordBot>()
constructor(private readonly core: ApiCoreService) {}

constructor(private readonly core: ApiCoreService, private readonly botMember: ApiBotMemberService) {}

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

const instance = new DiscordBot({ botId, token: bot.token })
await instance.start()
await this.botMember.setupListeners(bot, instance)
this.bots.set(bot.id, instance)

return true
Expand Down Expand Up @@ -167,11 +170,16 @@ export class ApiBotManagerService implements OnModuleInit {
console.log(`Can't find bot.`, botId, serverId)
return false
}
const community = await this.core.data.community.findFirst({ where: { bot: { id: botId } } })
if (!community) {
console.log(`Can't find community.`, botId, serverId)
return false
}
this.logger.verbose(`Fetching members... ${botId} ${serverId}`)

const [discordIdentityIds, botMemberIds] = await Promise.all([
this.getDiscordIdentityIds(),
this.getBotMemberIds(botId, serverId),
this.botMember.getDiscordIdentityIds(),
this.botMember.getBotMemberIds(botId, serverId),
])
const members = await bot.getDiscordServerMembers(serverId)

Expand All @@ -194,15 +202,19 @@ export class ApiBotManagerService implements OnModuleInit {
for (const member of filtered) {
const userId = member.id
// const identityProviderId = discordIdentityIds.includes(member.id) ? member.id : undefined
const created = await this.core.data.botMember.upsert({
where: { botId_userId_serverId: { botId, userId, serverId } },
update: {},
create: {
const created = await this.botMember.upsert({ botId, communityId: community.id, serverId, userId })
if (!created) {
this.logger.warn(`Failed to create bot member ${botId} ${serverId} ${userId}`)
continue
}

await this.core.logInfo(
community.id,
`Bot ${bot.client?.user?.username} added member ${member.user.username} to server ${serverId}`,
{
botId,
serverId,
userId,
},
})
)
this.logger.verbose(
`${botId} ${serverId} Processed member ${created.id} ${member.user.username} (linked: ${!!member.id})`,
)
Expand All @@ -215,29 +227,6 @@ export class ApiBotManagerService implements OnModuleInit {
)
return true
}

private async getBotMemberIds(botId: string, serverId: string) {
return this.core.data.botMember
.findMany({ where: { botId, serverId } })
.then((items) => items.map(({ userId }) => userId))
}

private 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' } } },
})
}
}

function convertPermissions(permissions: Record<PermissionsString, boolean>) {
Expand Down
137 changes: 137 additions & 0 deletions libs/api/bot/data-access/src/lib/api-bot-member.service.ts
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}`)
})
}
}
4 changes: 3 additions & 1 deletion libs/api/bot/data-access/src/lib/api-bot.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Injectable } from '@nestjs/common'
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 { ApiUserBotService } from './api-user-bot.service'

@Injectable()
export class ApiBotService {
constructor(
readonly manager: ApiBotManagerService,
readonly member: ApiBotMemberService,
readonly admin: ApiAdminBotService,
readonly user: ApiUserBotService,
) {}
Expand Down
2 changes: 2 additions & 0 deletions libs/api/bot/data-access/src/lib/helpers/api-bot.constants.ts
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'
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
}
}
}
Loading

0 comments on commit a189e8d

Please sign in to comment.