// SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, UserID, WebhookID, WebhookToken} from '../BrandedTypes'; import { BatchBuilder, deleteOneOrMany, fetchMany, fetchManyInChunks, fetchOne, } from '../database/CassandraQueryExecution'; import {buildPatchFromData, executeVersionedUpdate} from '../database/CassandraVersionedUpdate'; import type {WebhookRow} from '../database/types/ChannelTypes'; import {WEBHOOK_COLUMNS} from '../database/types/ChannelTypes'; import {Webhook} from '../models/Webhook'; import {Webhooks, WebhooksByChannel, WebhooksByGuild} from '../Tables'; import {IWebhookRepository} from './IWebhookRepository'; const FETCH_WEBHOOK_BY_ID_CQL = Webhooks.selectCql({ where: Webhooks.where.eq('webhook_id'), limit: 1, }); const FETCH_WEBHOOK_BY_TOKEN_CQL = Webhooks.selectCql({ where: [Webhooks.where.eq('webhook_id'), Webhooks.where.eq('webhook_token')], limit: 1, }); const FETCH_WEBHOOK_IDS_BY_GUILD_CQL = WebhooksByGuild.selectCql({ columns: ['webhook_id'], where: WebhooksByGuild.where.eq('guild_id'), }); const FETCH_WEBHOOK_IDS_BY_CHANNEL_CQL = WebhooksByChannel.selectCql({ columns: ['webhook_id'], where: WebhooksByChannel.where.eq('channel_id'), }); const FETCH_WEBHOOKS_BY_IDS_CQL = Webhooks.selectCql({ where: Webhooks.where.in('webhook_id', 'webhook_ids'), }); export class WebhookRepository extends IWebhookRepository { async findUnique(webhookId: WebhookID): Promise { const result = await fetchOne(FETCH_WEBHOOK_BY_ID_CQL, {webhook_id: webhookId}); return result ? new Webhook(result) : null; } async findByToken(webhookId: WebhookID, token: WebhookToken): Promise { const result = await fetchOne(FETCH_WEBHOOK_BY_TOKEN_CQL, { webhook_id: webhookId, webhook_token: token, }); return result ? new Webhook(result) : null; } async create(data: { webhookId: WebhookID; token: WebhookToken; type: number; guildId: GuildID | null; channelId: ChannelID | null; creatorId: UserID | null; name: string; avatarHash: string | null; }): Promise { const webhookData: WebhookRow = { webhook_id: data.webhookId, webhook_token: data.token, type: data.type, guild_id: data.guildId, channel_id: data.channelId, creator_id: data.creatorId, name: data.name, avatar_hash: data.avatarHash, version: 1, }; const result = await executeVersionedUpdate( async () => { return await fetchOne(FETCH_WEBHOOK_BY_ID_CQL, {webhook_id: data.webhookId}); }, (current) => ({ pk: {webhook_id: data.webhookId, webhook_token: data.token}, patch: buildPatchFromData(webhookData, current, WEBHOOK_COLUMNS, ['webhook_id', 'webhook_token']), }), Webhooks, ); const batch = new BatchBuilder(); if (data.guildId) { batch.addPrepared( WebhooksByGuild.upsertAll({ guild_id: data.guildId, webhook_id: data.webhookId, }), ); } if (data.channelId) { batch.addPrepared( WebhooksByChannel.upsertAll({ channel_id: data.channelId, webhook_id: data.webhookId, }), ); } await batch.execute(); return new Webhook({...webhookData, version: result.finalVersion ?? 1}); } async update( webhookId: WebhookID, data: Partial<{ token: WebhookToken; type: number; guildId: GuildID | null; channelId: ChannelID | null; creatorId: UserID | null; name: string; avatarHash: string | null; }>, oldData?: WebhookRow | null, ): Promise { const existing = oldData !== undefined ? (oldData ? new Webhook(oldData) : null) : await this.findUnique(webhookId); if (!existing) return null; const updatedData: WebhookRow = { webhook_id: webhookId, webhook_token: data.token ?? existing.token, type: data.type ?? existing.type, guild_id: data.guildId !== undefined ? data.guildId : existing.guildId, channel_id: data.channelId !== undefined ? data.channelId : existing.channelId, creator_id: data.creatorId !== undefined ? data.creatorId : existing.creatorId, name: data.name ?? existing.name, avatar_hash: data.avatarHash !== undefined ? data.avatarHash : existing.avatarHash, version: existing.version, }; const result = await executeVersionedUpdate( async () => fetchOne(FETCH_WEBHOOK_BY_ID_CQL, {webhook_id: webhookId}), (current) => ({ pk: {webhook_id: webhookId, webhook_token: updatedData.webhook_token}, patch: buildPatchFromData(updatedData, current, WEBHOOK_COLUMNS, ['webhook_id', 'webhook_token']), }), Webhooks, {initialData: oldData}, ); const batch = new BatchBuilder(); if (existing.guildId !== updatedData.guild_id) { if (existing.guildId) { batch.addPrepared( WebhooksByGuild.deleteByPk({ guild_id: existing.guildId, webhook_id: webhookId, }), ); } if (updatedData.guild_id) { batch.addPrepared( WebhooksByGuild.upsertAll({ guild_id: updatedData.guild_id, webhook_id: webhookId, }), ); } } if (existing.channelId !== updatedData.channel_id) { if (existing.channelId) { batch.addPrepared( WebhooksByChannel.deleteByPk({ channel_id: existing.channelId, webhook_id: webhookId, }), ); } if (updatedData.channel_id) { batch.addPrepared( WebhooksByChannel.upsertAll({ channel_id: updatedData.channel_id, webhook_id: webhookId, }), ); } } await batch.execute(); return new Webhook({...updatedData, version: result.finalVersion ?? 1}); } async delete(webhookId: WebhookID): Promise { const webhook = await this.findUnique(webhookId); if (!webhook) return; await deleteOneOrMany( Webhooks.deleteByPk({ webhook_id: webhookId, webhook_token: webhook.token, }), ); const batch = new BatchBuilder(); if (webhook.guildId) { batch.addPrepared( WebhooksByGuild.deleteByPk({ guild_id: webhook.guildId, webhook_id: webhookId, }), ); } if (webhook.channelId) { batch.addPrepared( WebhooksByChannel.deleteByPk({ channel_id: webhook.channelId, webhook_id: webhookId, }), ); } await batch.execute(); } private async fetchWebhooksByIds(webhookIds: Array): Promise> { if (webhookIds.length === 0) { return []; } const rows = await fetchManyInChunks(FETCH_WEBHOOKS_BY_IDS_CQL, webhookIds, (chunk) => ({ webhook_ids: chunk, })); return rows.map((row) => new Webhook(row)); } async listByGuild(guildId: GuildID): Promise> { const webhookIds = await fetchMany<{ webhook_id: bigint; }>(FETCH_WEBHOOK_IDS_BY_GUILD_CQL, {guild_id: guildId}); return this.fetchWebhooksByIds(webhookIds.map(({webhook_id}) => webhook_id)); } async listByChannel(channelId: ChannelID): Promise> { const webhookIds = await fetchMany<{ webhook_id: bigint; }>(FETCH_WEBHOOK_IDS_BY_CHANNEL_CQL, { channel_id: channelId, }); return this.fetchWebhooksByIds(webhookIds.map(({webhook_id}) => webhook_id)); } async countByGuild(guildId: GuildID): Promise { const webhookIds = await fetchMany<{ webhook_id: bigint; }>(FETCH_WEBHOOK_IDS_BY_GUILD_CQL, {guild_id: guildId}); return webhookIds.length; } async countByChannel(channelId: ChannelID): Promise { const webhookIds = await fetchMany<{ webhook_id: bigint; }>(FETCH_WEBHOOK_IDS_BY_CHANNEL_CQL, { channel_id: channelId, }); return webhookIds.length; } }