/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
import {
type ChannelID,
createWebhookID,
type GuildID,
type UserID,
type WebhookID,
type WebhookToken,
} from '~/BrandedTypes';
import {
BatchBuilder,
buildPatchFromData,
deleteOneOrMany,
executeVersionedUpdate,
fetchMany,
fetchOne,
} from '~/database/Cassandra';
import {WEBHOOK_COLUMNS, type WebhookRow} from '~/database/CassandraTypes';
import {Webhook} from '~/Models';
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'),
});
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,
{onFailure: 'log'},
);
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 () => {
if (oldData !== undefined) return oldData;
return await 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,
{onFailure: 'log'},
);
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();
}
async listByGuild(guildId: GuildID): Promise> {
const webhookIds = await fetchMany<{webhook_id: bigint}>(FETCH_WEBHOOK_IDS_BY_GUILD_CQL, {guild_id: guildId});
const webhooks: Array = [];
for (const {webhook_id} of webhookIds) {
const webhook = await this.findUnique(createWebhookID(webhook_id));
if (webhook) {
webhooks.push(webhook);
}
}
return webhooks;
}
async listByChannel(channelId: ChannelID): Promise> {
const webhookIds = await fetchMany<{webhook_id: bigint}>(FETCH_WEBHOOK_IDS_BY_CHANNEL_CQL, {
channel_id: channelId,
});
const webhooks: Array = [];
for (const {webhook_id} of webhookIds) {
const webhook = await this.findUnique(createWebhookID(webhook_id));
if (webhook) {
webhooks.push(webhook);
}
}
return webhooks;
}
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;
}
}