Files
fluxer/fluxer_api/src/api/invite/InviteRepository.ts
T

229 lines
6.0 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ChannelID, GuildID, InviteCode, UserID} from '@app/api/BrandedTypes';
import {createInviteCode} from '@app/api/BrandedTypes';
import {
BatchBuilder,
executeConditional,
fetchMany,
fetchOne,
upsertOne,
} from '@app/api/database/CassandraQueryExecution';
import {Db} from '@app/api/database/CassandraTypes';
import type {InviteRow} from '@app/api/database/types/ChannelTypes';
import {IInviteRepository} from '@app/api/invite/IInviteRepository';
import {Invite} from '@app/api/models/Invite';
import {Invites, InvitesByChannel, InvitesByGuild} from '@app/api/Tables';
const FETCH_INVITE_BY_CODE_CQL = Invites.selectCql({
where: Invites.where.eq('code'),
limit: 1,
});
const FETCH_INVITES_BY_CHANNEL_CQL = InvitesByChannel.selectCql({
columns: ['code'],
where: InvitesByChannel.where.eq('channel_id'),
});
const FETCH_INVITES_BY_GUILD_CQL = InvitesByGuild.selectCql({
columns: ['code'],
where: InvitesByGuild.where.eq('guild_id'),
});
interface CreateInviteParams {
code: InviteCode;
type: number;
guild_id: GuildID;
channel_id?: ChannelID | null;
inviter_id?: UserID | null;
uses: number;
max_uses: number;
max_age: number;
temporary?: boolean;
}
export class InviteRepository extends IInviteRepository {
async findUnique(code: InviteCode): Promise<Invite | null> {
const invite = await fetchOne<InviteRow>(FETCH_INVITE_BY_CODE_CQL, {code});
return invite ? new Invite(invite) : null;
}
async listChannelInvites(channelId: ChannelID): Promise<Array<Invite>> {
const inviteCodes = await fetchMany<{
code: string;
}>(FETCH_INVITES_BY_CHANNEL_CQL, {channel_id: channelId});
if (inviteCodes.length === 0) return [];
const invites: Array<Invite> = [];
for (const {code} of inviteCodes) {
const invite = await this.findUnique(createInviteCode(code));
if (invite) invites.push(invite);
}
return invites;
}
async listGuildInvites(guildId: GuildID): Promise<Array<Invite>> {
const inviteCodes = await fetchMany<{
code: string;
}>(FETCH_INVITES_BY_GUILD_CQL, {guild_id: guildId});
if (inviteCodes.length === 0) return [];
const invites: Array<Invite> = [];
for (const {code} of inviteCodes) {
const invite = await this.findUnique(createInviteCode(code));
if (invite) invites.push(invite);
}
return invites;
}
async create(data: CreateInviteParams): Promise<Invite> {
const inviteRow: InviteRow = {
code: data.code,
type: data.type,
guild_id: data.guild_id,
channel_id: data.channel_id ?? null,
inviter_id: data.inviter_id ?? null,
created_at: new Date(),
uses: data.uses,
max_uses: data.max_uses,
max_age: data.max_age,
temporary: data.temporary ?? false,
version: 1,
};
const batch = new BatchBuilder();
const hasExpiry = inviteRow.max_age > 0;
if (hasExpiry) {
batch.addPrepared(Invites.insertWithTtlParam(inviteRow, 'max_age'));
} else {
batch.addPrepared(Invites.insert(inviteRow));
}
if (inviteRow.guild_id) {
batch.addPrepared(
hasExpiry
? InvitesByGuild.insertWithTtl(
{
guild_id: inviteRow.guild_id,
code: inviteRow.code,
},
inviteRow.max_age,
)
: InvitesByGuild.insert({
guild_id: inviteRow.guild_id,
code: inviteRow.code,
}),
);
}
if (inviteRow.channel_id) {
batch.addPrepared(
hasExpiry
? InvitesByChannel.insertWithTtl(
{
channel_id: inviteRow.channel_id,
code: inviteRow.code,
},
inviteRow.max_age,
)
: InvitesByChannel.insert({
channel_id: inviteRow.channel_id,
code: inviteRow.code,
}),
);
}
await batch.execute();
if (hasExpiry) {
await upsertOne(Invites.upsertAllWithTtl(inviteRow, inviteRow.max_age));
if (inviteRow.guild_id) {
await upsertOne(
InvitesByGuild.upsertAllWithTtl(
{
guild_id: inviteRow.guild_id,
code: inviteRow.code,
},
inviteRow.max_age,
),
);
}
if (inviteRow.channel_id) {
await upsertOne(
InvitesByChannel.upsertAllWithTtl(
{
channel_id: inviteRow.channel_id,
code: inviteRow.code,
},
inviteRow.max_age,
),
);
}
} else {
await upsertOne(Invites.upsertAll(inviteRow));
if (inviteRow.guild_id) {
await upsertOne(
InvitesByGuild.upsertAll({
guild_id: inviteRow.guild_id,
code: inviteRow.code,
}),
);
}
if (inviteRow.channel_id) {
await upsertOne(
InvitesByChannel.upsertAll({
channel_id: inviteRow.channel_id,
code: inviteRow.code,
}),
);
}
}
return new Invite(inviteRow);
}
async updateInviteUses(code: InviteCode, uses: number, invite: Invite): Promise<void> {
if (invite.maxAge > 0) {
await upsertOne(
Invites.patchByPkWithTtl(
{code},
{
uses: Db.set(uses),
},
this.remainingTtl(invite),
),
);
} else {
await upsertOne(
Invites.patchByPk(
{code},
{
uses: Db.set(uses),
},
),
);
}
}
async compareAndSetInviteUses(invite: Invite, uses: number): Promise<boolean> {
const patch = {uses: Db.set(uses)};
const expected = {uses: invite.uses};
if (invite.maxAge > 0) {
return executeConditional(
Invites.conditionalPatchByPkWithTtl({code: invite.code}, patch, expected, this.remainingTtl(invite)),
);
}
return executeConditional(Invites.conditionalPatchByPk({code: invite.code}, patch, expected));
}
private remainingTtl(invite: Invite): number {
return Math.max(Math.floor((invite.createdAt.getTime() + invite.maxAge * 1000 - Date.now()) / 1000), 1);
}
async delete(code: InviteCode): Promise<void> {
const invite = await this.findUnique(code);
if (!invite) {
return;
}
const batch = new BatchBuilder();
batch.addPrepared(Invites.deleteByPk({code}));
if (invite.guildId) {
batch.addPrepared(InvitesByGuild.deleteByPk({guild_id: invite.guildId, code}));
}
if (invite.channelId) {
batch.addPrepared(InvitesByChannel.deleteByPk({channel_id: invite.channelId, code}));
}
await batch.execute();
}
}