feat(futo-backups-bot): /claim-backups-role grants the customer role (#567)

This commit is contained in:
Antoine Lecompte
2026-08-26 09:58:48 -04:00
committed by GitHub
parent 769572dea8
commit f7e60f0d41
14 changed files with 266 additions and 11 deletions
+13 -1
View File
@@ -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 |
+1
View File
@@ -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',
} }
+4
View File
@@ -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({
+4 -1
View File
@@ -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([]),
+3
View File
@@ -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';
+3
View File
@@ -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"
+3
View File
@@ -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 = ""
}