mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 21:37:50 +08:00
feat(futo-backups-bot): /claim-backups-role grants the customer role (#567)
This commit is contained in:
+13
-1
@@ -53,6 +53,18 @@ belongs to a known account. Staff can bypass it with **`/ticket user:<user>`**
|
|||||||
and the staff note records whether a linked account exists. Pre-signup
|
and the staff note records whether a linked account exists. Pre-signup
|
||||||
questions stay in public channels or go through that override.
|
questions stay in public channels or go through that override.
|
||||||
|
|
||||||
|
## Channels and the customer role
|
||||||
|
|
||||||
|
The FUTO Backups category is private (Team/FUTO/Yucca/Contributor/Support
|
||||||
|
Crew see it; flipping it public later is the rollout switch). `#general` is
|
||||||
|
open chat for everyone who sees the category plus customers. The
|
||||||
|
**`/claim-backups-role`** command (also offered by a daily bot prompt in
|
||||||
|
`#general`, skipped when the channel is quiet) runs the same link flow as
|
||||||
|
support and then grants the plain **FUTO Backups** role — linked customers get it instantly,
|
||||||
|
unlinked ones link first. The role unlocks `#customer` (customer chat, also visible to
|
||||||
|
Admin/Team/Yucca/FUTO). `#support` stays visible to everyone;
|
||||||
|
ticket threads live under it as before.
|
||||||
|
|
||||||
## Tickets: Discord is the source of truth
|
## Tickets: Discord is the source of truth
|
||||||
|
|
||||||
No ticket table. A ticket is a **private thread** under the support channel;
|
No ticket table. A ticket is a **private thread** under the support channel;
|
||||||
@@ -95,7 +107,7 @@ uses the dev guild through `.env`.
|
|||||||
| Variable | Source |
|
| Variable | Source |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DISCORD_BOT_TOKEN` | Secret ← `YUCCA_DISCORD_BOT_TOKEN` (manual item) |
|
| `DISCORD_BOT_TOKEN` | Secret ← `YUCCA_DISCORD_BOT_TOKEN` (manual item) |
|
||||||
| `DISCORD_GUILD_ID`, `DISCORD_STAFF_ROLE_ID`, `DISCORD_SUPPORT_CHANNEL_ID` | Secret ← `YUCCA_DISCORD_SUPPORT_IDS` (written by core-infra-tf's discord apply) |
|
| `DISCORD_GUILD_ID`, `DISCORD_STAFF_ROLE_ID`, `DISCORD_SUPPORT_CHANNEL_ID`, `DISCORD_GENERAL_CHANNEL_ID`, `DISCORD_CHAT_CHANNEL_ID`, `DISCORD_CUSTOMER_ROLE_ID` | Secret ← `YUCCA_DISCORD_SUPPORT_IDS` (written by core-infra-tf's discord apply) |
|
||||||
| `INTERNAL_SECRET` | Secret ← TF-generated (`random_password`, shared with yucca-api) |
|
| `INTERNAL_SECRET` | Secret ← TF-generated (`random_password`, shared with yucca-api) |
|
||||||
| `TRANSCRIPT_S3_ACCESS_KEY_ID` / `..._SECRET_ACCESS_KEY` | Secret ← ceph-stack-minted `*_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_*` |
|
| `TRANSCRIPT_S3_ACCESS_KEY_ID` / `..._SECRET_ACCESS_KEY` | Secret ← ceph-stack-minted `*_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_*` |
|
||||||
| `TRANSCRIPT_S3_ENDPOINT`, `TRANSCRIPT_S3_BUCKET` | cluster-settings |
|
| `TRANSCRIPT_S3_ENDPOINT`, `TRANSCRIPT_S3_BUCKET` | cluster-settings |
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export enum ComponentId {
|
|||||||
CloseTicket = 'close-ticket',
|
CloseTicket = 'close-ticket',
|
||||||
TicketModal = 'ticket-modal',
|
TicketModal = 'ticket-modal',
|
||||||
TicketDescription = 'ticket-description',
|
TicketDescription = 'ticket-description',
|
||||||
|
ClaimRole = 'claim-role',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const schema = z.object({
|
|||||||
DISCORD_GUILD_ID: z.string().default(''),
|
DISCORD_GUILD_ID: z.string().default(''),
|
||||||
DISCORD_STAFF_ROLE_ID: z.string().default(''),
|
DISCORD_STAFF_ROLE_ID: z.string().default(''),
|
||||||
DISCORD_SUPPORT_CHANNEL_ID: z.string().default(''),
|
DISCORD_SUPPORT_CHANNEL_ID: z.string().default(''),
|
||||||
|
DISCORD_GENERAL_CHANNEL_ID: z.string().default(''),
|
||||||
|
DISCORD_CHAT_CHANNEL_ID: z.string().default(''),
|
||||||
|
DISCORD_CUSTOMER_ROLE_ID: z.string().default(''),
|
||||||
|
|
||||||
YUCCA_API_URL: z.url().default('http://localhost:3020'),
|
YUCCA_API_URL: z.url().default('http://localhost:3020'),
|
||||||
INTERNAL_SECRET: z.string().default(''),
|
INTERNAL_SECRET: z.string().default(''),
|
||||||
@@ -20,6 +23,7 @@ const schema = z.object({
|
|||||||
|
|
||||||
TICKET_RETENTION_DAYS: z.coerce.number().default(14),
|
TICKET_RETENTION_DAYS: z.coerce.number().default(14),
|
||||||
TICKET_USER_LIMIT: z.coerce.number().int().positive().default(3),
|
TICKET_USER_LIMIT: z.coerce.number().int().positive().default(3),
|
||||||
|
CLAIM_PROMPT_MIN_MESSAGES: z.coerce.number().int().positive().default(25),
|
||||||
|
|
||||||
TRANSCRIPT_S3_ENDPOINT: z.string().default(''),
|
TRANSCRIPT_S3_ENDPOINT: z.string().default(''),
|
||||||
TRANSCRIPT_S3_BUCKET: z.string().default(''),
|
TRANSCRIPT_S3_BUCKET: z.string().default(''),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
Guild,
|
Guild,
|
||||||
Interaction,
|
Interaction,
|
||||||
|
Message,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
TextChannel,
|
TextChannel,
|
||||||
@@ -57,11 +58,32 @@ export class DiscordRepository {
|
|||||||
.setName('staff-notes')
|
.setName('staff-notes')
|
||||||
.setDescription("Link this ticket's staff-notes thread (staff only)")
|
.setDescription("Link this ticket's staff-notes thread (staff only)")
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('claim-backups-role')
|
||||||
|
.setDescription('Claim the FUTO Backups customer role for your linked account')
|
||||||
|
.toJSON(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensurePinnedSupportMessage(message: MessageCreateOptions, buttonId: string): Promise<void> {
|
async listRecentMessages(channelId: string, limit: number): Promise<Message[]> {
|
||||||
const channel = await this.supportChannel();
|
const channel = await this.textChannel(channelId);
|
||||||
|
const messages = await channel.messages.fetch({ limit });
|
||||||
|
return [...messages.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(channelId: string, message: MessageCreateOptions): Promise<void> {
|
||||||
|
const channel = await this.textChannel(channelId);
|
||||||
|
await channel.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRoleToMember(discordUserId: string, roleId: string): Promise<void> {
|
||||||
|
const guild = await this.guild();
|
||||||
|
const member = await guild.members.fetch(discordUserId);
|
||||||
|
await member.roles.add(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensurePinnedMessage(channelId: string, message: MessageCreateOptions, buttonId: string): Promise<void> {
|
||||||
|
const channel = await this.textChannel(channelId);
|
||||||
const [pinned, recent] = await Promise.all([
|
const [pinned, recent] = await Promise.all([
|
||||||
channel.messages.fetchPinned(),
|
channel.messages.fetchPinned(),
|
||||||
channel.messages.fetch({ limit: 100 }),
|
channel.messages.fetch({ limit: 100 }),
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ const newModalInteraction = (description: string, overrides: object = {}) =>
|
|||||||
...overrides,
|
...overrides,
|
||||||
}) as never;
|
}) as never;
|
||||||
|
|
||||||
|
const humanMessage = (ts: number) => ({ author: { bot: false }, createdTimestamp: ts, components: [] }) as never;
|
||||||
|
const promptMessage = (ts: number, del = jest.fn().mockResolvedValue(void 0)) =>
|
||||||
|
({
|
||||||
|
author: { bot: true },
|
||||||
|
createdTimestamp: ts,
|
||||||
|
components: [{ components: [{ customId: ComponentId.ClaimRole }] }],
|
||||||
|
delete: del,
|
||||||
|
}) as never;
|
||||||
|
|
||||||
const asThread = (properties: object): ThreadChannel =>
|
const asThread = (properties: object): ThreadChannel =>
|
||||||
Object.assign(Object.create(ThreadChannel.prototype) as ThreadChannel, properties);
|
Object.assign(Object.create(ThreadChannel.prototype) as ThreadChannel, properties);
|
||||||
|
|
||||||
@@ -210,6 +219,73 @@ describe(SupportService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('/claim-backups-role command', () => {
|
||||||
|
it('grants the customer role to a linked user', async () => {
|
||||||
|
mocks.api.getLink.mockResolvedValue(link);
|
||||||
|
const interaction = newCommandInteraction(
|
||||||
|
{},
|
||||||
|
{ commandName: 'claim-backups-role', user: { id: '123456789', username: 'Someone' } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.handleInteraction(interaction);
|
||||||
|
|
||||||
|
expect(mocks.discord.addRoleToMember).toHaveBeenCalledWith('123456789', 'customer-role');
|
||||||
|
expect((interaction as { editReply: jest.Mock }).editReply).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ content: expect.stringContaining('chat-channel') }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grants the role after linking for an unlinked user', async () => {
|
||||||
|
mocks.api.getLink.mockResolvedValueOnce(null).mockResolvedValue(link);
|
||||||
|
mocks.api.createLinkRequest.mockResolvedValue({ code: 'code-1', expiresAt: new Date(Date.now() + 2000) });
|
||||||
|
const interaction = newCommandInteraction(
|
||||||
|
{},
|
||||||
|
{ commandName: 'claim-backups-role', user: { id: '123456789', username: 'Someone' } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.handleInteraction(interaction);
|
||||||
|
|
||||||
|
expect(mocks.api.createLinkRequest).toHaveBeenCalledWith('123456789', 'Someone');
|
||||||
|
expect(mocks.discord.addRoleToMember).toHaveBeenCalledWith('123456789', 'customer-role');
|
||||||
|
expect((interaction as { editReply: jest.Mock }).editReply).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ content: expect.stringContaining('role claimed') }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('daily claim prompt', () => {
|
||||||
|
it('skips a quiet channel', async () => {
|
||||||
|
mocks.discord.listRecentMessages.mockResolvedValue([humanMessage(1), humanMessage(2)]);
|
||||||
|
|
||||||
|
await sut.postClaimPrompt();
|
||||||
|
|
||||||
|
expect(mocks.discord.sendMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces the old prompt when the channel is active', async () => {
|
||||||
|
const del = jest.fn().mockResolvedValue(void 0);
|
||||||
|
const messages = [promptMessage(100, del), ...Array.from({ length: 30 }, (_, i) => humanMessage(200 + i))];
|
||||||
|
mocks.discord.listRecentMessages.mockResolvedValue(messages);
|
||||||
|
|
||||||
|
await sut.postClaimPrompt();
|
||||||
|
|
||||||
|
expect(del).toHaveBeenCalled();
|
||||||
|
expect(mocks.discord.sendMessage).toHaveBeenCalledWith(
|
||||||
|
'general-channel',
|
||||||
|
expect.objectContaining({ content: expect.stringContaining('chat-channel') }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores activity that predates the last prompt', async () => {
|
||||||
|
const messages = [...Array.from({ length: 30 }, (_, i) => humanMessage(10 + i)), promptMessage(100)];
|
||||||
|
mocks.discord.listRecentMessages.mockResolvedValue(messages);
|
||||||
|
|
||||||
|
await sut.postClaimPrompt();
|
||||||
|
|
||||||
|
expect(mocks.discord.sendMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('/staff-notes command', () => {
|
describe('/staff-notes command', () => {
|
||||||
it('links the paired staff thread for staff inside a ticket', async () => {
|
it('links the paired staff thread for staff inside a ticket', async () => {
|
||||||
const thread = asThread({ id: 'thread-1', name: 'ticket-someone-6789-x', parentId: 'support-channel' });
|
const thread = asThread({ id: 'thread-1', name: 'ticket-someone-6789-x', parentId: 'support-channel' });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LoggerRepository } from '@common/server/otel';
|
import { LoggerRepository } from '@common/server/otel';
|
||||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import {
|
import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
ChatInputCommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
Interaction,
|
Interaction,
|
||||||
|
Message,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
ModalBuilder,
|
ModalBuilder,
|
||||||
ModalSubmitInteraction,
|
ModalSubmitInteraction,
|
||||||
@@ -61,7 +63,8 @@ export class SupportService implements OnApplicationBootstrap {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error, 'failed to register slash commands — is the applications.commands scope granted?');
|
this.logger.error(error, 'failed to register slash commands — is the applications.commands scope granted?');
|
||||||
}
|
}
|
||||||
await this.discord.ensurePinnedSupportMessage(
|
await this.discord.ensurePinnedMessage(
|
||||||
|
env.DISCORD_SUPPORT_CHANNEL_ID,
|
||||||
{
|
{
|
||||||
content: 'Need help with FUTO Backups? Click below to open a private ticket with our staff.',
|
content: 'Need help with FUTO Backups? Click below to open a private ticket with our staff.',
|
||||||
components: [this.buttonRow(ComponentId.OpenTicket, 'Get support')],
|
components: [this.buttonRow(ComponentId.OpenTicket, 'Get support')],
|
||||||
@@ -81,6 +84,9 @@ export class SupportService implements OnApplicationBootstrap {
|
|||||||
case ComponentId.CloseTicket: {
|
case ComponentId.CloseTicket: {
|
||||||
return await this.onCloseRequested(interaction);
|
return await this.onCloseRequested(interaction);
|
||||||
}
|
}
|
||||||
|
case ComponentId.ClaimRole: {
|
||||||
|
return await this.onClaimRequested(interaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (interaction.isModalSubmit() && interaction.customId === ComponentId.TicketModal) {
|
if (interaction.isModalSubmit() && interaction.customId === ComponentId.TicketModal) {
|
||||||
@@ -92,6 +98,9 @@ export class SupportService implements OnApplicationBootstrap {
|
|||||||
if (interaction.isChatInputCommand() && interaction.commandName === 'staff-notes') {
|
if (interaction.isChatInputCommand() && interaction.commandName === 'staff-notes') {
|
||||||
return await this.onStaffNotesRequested(interaction);
|
return await this.onStaffNotesRequested(interaction);
|
||||||
}
|
}
|
||||||
|
if (interaction.isChatInputCommand() && interaction.commandName === 'claim-backups-role') {
|
||||||
|
return await this.onClaimRequested(interaction);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error, 'failed to handle interaction');
|
this.logger.error(error, 'failed to handle interaction');
|
||||||
if (!interaction.isButton() && !interaction.isModalSubmit() && !interaction.isChatInputCommand()) {
|
if (!interaction.isButton() && !interaction.isModalSubmit() && !interaction.isChatInputCommand()) {
|
||||||
@@ -142,7 +151,79 @@ export class SupportService implements OnApplicationBootstrap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startLinkFlow(interaction: ButtonInteraction) {
|
private async onClaimRequested(interaction: ButtonInteraction | ChatInputCommandInteraction) {
|
||||||
|
if (!env.DISCORD_CUSTOMER_ROLE_ID) {
|
||||||
|
await interaction.reply({ content: 'Role claiming is not available yet.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let link: DiscordLink | null;
|
||||||
|
try {
|
||||||
|
link = await withTimeout(this.api.getLink(interaction.user.id), LINK_LOOKUP_TIMEOUT_MS);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error, 'link lookup failed');
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Role claiming is temporarily unavailable, please try again.',
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
if (!link) {
|
||||||
|
return this.startLinkFlow(interaction, async () => {
|
||||||
|
await this.discord.addRoleToMember(interaction.user.id, env.DISCORD_CUSTOMER_ROLE_ID);
|
||||||
|
return { content: `Account linked and role claimed — welcome!${this.chatMention()}` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.discord.addRoleToMember(interaction.user.id, env.DISCORD_CUSTOMER_ROLE_ID);
|
||||||
|
await interaction.editReply({ content: `Role claimed — welcome!${this.chatMention()}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_NOON)
|
||||||
|
async postClaimPrompt() {
|
||||||
|
if (!this.discord.enabled || !env.DISCORD_GENERAL_CHANNEL_ID || !env.DISCORD_CUSTOMER_ROLE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await this.discord.listRecentMessages(env.DISCORD_GENERAL_CHANNEL_ID, 50);
|
||||||
|
const isPrompt = (message: Message) =>
|
||||||
|
message.author.bot &&
|
||||||
|
message.components.some(
|
||||||
|
(row) =>
|
||||||
|
'components' in row &&
|
||||||
|
row.components.some((component) => 'customId' in component && component.customId === ComponentId.ClaimRole),
|
||||||
|
);
|
||||||
|
const lastPrompt = messages
|
||||||
|
.filter((message) => isPrompt(message))
|
||||||
|
.toSorted((a, b) => b.createdTimestamp - a.createdTimestamp)[0];
|
||||||
|
const activity = messages.filter(
|
||||||
|
(message) => !message.author.bot && (!lastPrompt || message.createdTimestamp > lastPrompt.createdTimestamp),
|
||||||
|
);
|
||||||
|
if (activity.length < env.CLAIM_PROMPT_MIN_MESSAGES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPrompt) {
|
||||||
|
await lastPrompt
|
||||||
|
.delete()
|
||||||
|
.catch((error: unknown) => this.logger.warn(error, 'could not delete the previous claim prompt'));
|
||||||
|
}
|
||||||
|
await this.discord.sendMessage(env.DISCORD_GENERAL_CHANNEL_ID, {
|
||||||
|
content: `FUTO Backups customer? Claim your role to unlock${env.DISCORD_CHAT_CHANNEL_ID ? ` <#${env.DISCORD_CHAT_CHANNEL_ID}>` : ' the customer chat'}.`,
|
||||||
|
components: [this.buttonRow(ComponentId.ClaimRole, 'Claim your role')],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private chatMention(): string {
|
||||||
|
return env.DISCORD_CHAT_CHANNEL_ID ? ` Say hi in <#${env.DISCORD_CHAT_CHANNEL_ID}>.` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startLinkFlow(
|
||||||
|
interaction: ButtonInteraction | ChatInputCommandInteraction,
|
||||||
|
onLinked?: () => Promise<{ content: string; components?: ActionRowBuilder<ButtonBuilder>[] }>,
|
||||||
|
) {
|
||||||
const { code, expiresAt } = await this.api.createLinkRequest(interaction.user.id, interaction.user.username);
|
const { code, expiresAt } = await this.api.createLinkRequest(interaction.user.id, interaction.user.username);
|
||||||
const url = `${env.WEB_URL}/link/discord?code=${code}`;
|
const url = `${env.WEB_URL}/link/discord?code=${code}`;
|
||||||
|
|
||||||
@@ -156,15 +237,18 @@ export class SupportService implements OnApplicationBootstrap {
|
|||||||
});
|
});
|
||||||
|
|
||||||
while (Date.now() < expiresAt.getTime()) {
|
while (Date.now() < expiresAt.getTime()) {
|
||||||
await sleep(LINK_POLL_INTERVAL_MS);
|
|
||||||
const link = await this.api.getLink(interaction.user.id).catch(() => null);
|
const link = await this.api.getLink(interaction.user.id).catch(() => null);
|
||||||
if (link) {
|
if (link) {
|
||||||
await interaction.editReply({
|
const success = onLinked
|
||||||
content: 'Account linked! Now open your ticket.',
|
? await onLinked()
|
||||||
components: [this.buttonRow(ComponentId.CreateTicket, 'Open ticket')],
|
: {
|
||||||
});
|
content: 'Account linked! Now open your ticket.',
|
||||||
|
components: [this.buttonRow(ComponentId.CreateTicket, 'Open ticket')],
|
||||||
|
};
|
||||||
|
await interaction.editReply({ components: [], ...success });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await sleep(LINK_POLL_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export const newDiscordRepositoryMock = (): jest.Mocked<RepositoryInterface<Disc
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
start: jest.fn(),
|
start: jest.fn(),
|
||||||
registerCommands: jest.fn(),
|
registerCommands: jest.fn(),
|
||||||
ensurePinnedSupportMessage: jest.fn(),
|
ensurePinnedMessage: jest.fn(),
|
||||||
|
addRoleToMember: jest.fn(),
|
||||||
|
listRecentMessages: jest.fn().mockResolvedValue([]),
|
||||||
|
sendMessage: jest.fn(),
|
||||||
createTicketThread: jest.fn(),
|
createTicketThread: jest.fn(),
|
||||||
createStaffThread: jest.fn(),
|
createStaffThread: jest.fn(),
|
||||||
listOpenTicketThreads: jest.fn().mockResolvedValue([]),
|
listOpenTicketThreads: jest.fn().mockResolvedValue([]),
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ process.env.FUTO_BACKUPS_BOT_PORT ??= '3050';
|
|||||||
process.env.DISCORD_GUILD_ID ??= 'guild';
|
process.env.DISCORD_GUILD_ID ??= 'guild';
|
||||||
process.env.DISCORD_STAFF_ROLE_ID ??= 'staff-role';
|
process.env.DISCORD_STAFF_ROLE_ID ??= 'staff-role';
|
||||||
process.env.DISCORD_SUPPORT_CHANNEL_ID ??= 'support-channel';
|
process.env.DISCORD_SUPPORT_CHANNEL_ID ??= 'support-channel';
|
||||||
|
process.env.DISCORD_GENERAL_CHANNEL_ID ??= 'general-channel';
|
||||||
|
process.env.DISCORD_CHAT_CHANNEL_ID ??= 'chat-channel';
|
||||||
|
process.env.DISCORD_CUSTOMER_ROLE_ID ??= 'customer-role';
|
||||||
|
|||||||
@@ -94,5 +94,8 @@ export TF_VAR_yucca_discord_bot_token="op://yucca_tf_staging/YUCCA_DISCORD_BOT_T
|
|||||||
export TF_VAR_yucca_discord_guild_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/guild_id"
|
export TF_VAR_yucca_discord_guild_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/guild_id"
|
||||||
export TF_VAR_yucca_discord_staff_role_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/staff_role_id"
|
export TF_VAR_yucca_discord_staff_role_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/staff_role_id"
|
||||||
export TF_VAR_yucca_discord_support_channel_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/support_channel_id"
|
export TF_VAR_yucca_discord_support_channel_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/support_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_general_channel_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/general_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_chat_channel_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/chat_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_customer_role_id="op://yucca_tf_staging/YUCCA_DISCORD_SUPPORT_IDS/discord/customer_role_id"
|
||||||
export TF_VAR_sietch_transcripts_access_key="op://yucca_tf_staging/SIETCH_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_ACCESS_KEY/password"
|
export TF_VAR_sietch_transcripts_access_key="op://yucca_tf_staging/SIETCH_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_ACCESS_KEY/password"
|
||||||
export TF_VAR_sietch_transcripts_secret_key="op://yucca_tf_staging/SIETCH_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_SECRET_KEY/password"
|
export TF_VAR_sietch_transcripts_secret_key="op://yucca_tf_staging/SIETCH_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_SECRET_KEY/password"
|
||||||
|
|||||||
@@ -94,5 +94,8 @@ export TF_VAR_vmauth_remote_write_password="op://shared_tf_prod/O11Y_VICTORIAMET
|
|||||||
# export TF_VAR_yucca_discord_guild_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/guild_id"
|
# export TF_VAR_yucca_discord_guild_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/guild_id"
|
||||||
# export TF_VAR_yucca_discord_staff_role_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/staff_role_id"
|
# export TF_VAR_yucca_discord_staff_role_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/staff_role_id"
|
||||||
# export TF_VAR_yucca_discord_support_channel_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/support_channel_id"
|
# export TF_VAR_yucca_discord_support_channel_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/support_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_general_channel_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/general_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_chat_channel_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/chat_channel_id"
|
||||||
|
# export TF_VAR_yucca_discord_customer_role_id="op://yucca_tf_prod/YUCCA_DISCORD_SUPPORT_IDS/discord/customer_role_id"
|
||||||
# export TF_VAR_spice_transcripts_access_key="op://yucca_tf_prod/SPICE_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_ACCESS_KEY/password"
|
# export TF_VAR_spice_transcripts_access_key="op://yucca_tf_prod/SPICE_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_ACCESS_KEY/password"
|
||||||
# export TF_VAR_spice_transcripts_secret_key="op://yucca_tf_prod/SPICE_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_SECRET_KEY/password"
|
# export TF_VAR_spice_transcripts_secret_key="op://yucca_tf_prod/SPICE_CEPH_S3_SVC_YUCCA_TRANSCRIPTS_SECRET_KEY/password"
|
||||||
|
|||||||
@@ -253,6 +253,9 @@ resource "kubernetes_secret_v1" "futo_backups_bot" {
|
|||||||
DISCORD_GUILD_ID = var.yucca_discord_guild_id
|
DISCORD_GUILD_ID = var.yucca_discord_guild_id
|
||||||
DISCORD_STAFF_ROLE_ID = var.yucca_discord_staff_role_id
|
DISCORD_STAFF_ROLE_ID = var.yucca_discord_staff_role_id
|
||||||
DISCORD_SUPPORT_CHANNEL_ID = var.yucca_discord_support_channel_id
|
DISCORD_SUPPORT_CHANNEL_ID = var.yucca_discord_support_channel_id
|
||||||
|
DISCORD_GENERAL_CHANNEL_ID = var.yucca_discord_general_channel_id
|
||||||
|
DISCORD_CHAT_CHANNEL_ID = var.yucca_discord_chat_channel_id
|
||||||
|
DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id
|
||||||
TRANSCRIPT_S3_ACCESS_KEY_ID = var.spice_transcripts_access_key
|
TRANSCRIPT_S3_ACCESS_KEY_ID = var.spice_transcripts_access_key
|
||||||
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.spice_transcripts_secret_key
|
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.spice_transcripts_secret_key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,3 +287,22 @@ variable "yucca_discord_support_channel_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
variable "yucca_discord_chat_channel_id" {
|
||||||
|
description = "#chat channel id mentioned after a role claim (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "yucca_discord_customer_role_id" {
|
||||||
|
description = "FUTO Backups customer role id futo-backups-bot assigns on claim (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "yucca_discord_general_channel_id" {
|
||||||
|
description = "#general channel id for futo-backups-bot's daily claim prompt (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -251,6 +251,9 @@ resource "kubernetes_secret_v1" "futo_backups_bot" {
|
|||||||
DISCORD_GUILD_ID = var.yucca_discord_guild_id
|
DISCORD_GUILD_ID = var.yucca_discord_guild_id
|
||||||
DISCORD_STAFF_ROLE_ID = var.yucca_discord_staff_role_id
|
DISCORD_STAFF_ROLE_ID = var.yucca_discord_staff_role_id
|
||||||
DISCORD_SUPPORT_CHANNEL_ID = var.yucca_discord_support_channel_id
|
DISCORD_SUPPORT_CHANNEL_ID = var.yucca_discord_support_channel_id
|
||||||
|
DISCORD_GENERAL_CHANNEL_ID = var.yucca_discord_general_channel_id
|
||||||
|
DISCORD_CHAT_CHANNEL_ID = var.yucca_discord_chat_channel_id
|
||||||
|
DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id
|
||||||
TRANSCRIPT_S3_ACCESS_KEY_ID = var.sietch_transcripts_access_key
|
TRANSCRIPT_S3_ACCESS_KEY_ID = var.sietch_transcripts_access_key
|
||||||
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.sietch_transcripts_secret_key
|
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.sietch_transcripts_secret_key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,3 +260,22 @@ variable "yucca_discord_support_channel_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
variable "yucca_discord_chat_channel_id" {
|
||||||
|
description = "#chat channel id mentioned after a role claim (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "yucca_discord_customer_role_id" {
|
||||||
|
description = "FUTO Backups customer role id futo-backups-bot assigns on claim (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "yucca_discord_general_channel_id" {
|
||||||
|
description = "#general channel id for futo-backups-bot's daily claim prompt (YUCCA_DISCORD_SUPPORT_IDS)."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user