diff --git a/.changeset/wicked-flowers-battle.md b/.changeset/wicked-flowers-battle.md new file mode 100644 index 0000000..d3d2957 --- /dev/null +++ b/.changeset/wicked-flowers-battle.md @@ -0,0 +1,5 @@ +--- +"tako": minor +--- + +Added a /crosspost command that automatically publishes messages in an announcement channel. diff --git a/apps/docs/src/content/docs/en/commands/utility/crosspost.mdx b/apps/docs/src/content/docs/en/commands/utility/crosspost.mdx new file mode 100644 index 0000000..ba23495 --- /dev/null +++ b/apps/docs/src/content/docs/en/commands/utility/crosspost.mdx @@ -0,0 +1,44 @@ +--- +title: Crosspost +description: | + A reference page for the '/crosspost' command inside the Tako Discord app. + With '/crosspost', you can setup the app to automatically publish all messages in an announcement channel. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import Badge from '@astrojs/starlight/components/Badge.astro'; + +With `/crosspost`, you can setup the app to automatically publish all messages in an announcement channel. + +## Usage + + + + `/crosspost [channel] [state]` + + **Example**: + `/crosspost channel:#news state:true` will enable crossposting for the `#news` channel. + + + +## Permissions + +In order to use the `/crosspost` subcommands, you need the `Manage Channels` & `Manage Messages` permisions. +:::tip +You can change the permisions via `Server Settings > Integrations > Tako` in your Discord server. +::: + +## Options + + + + + The channel to set the state of whether the app should automatically publish all new messages from or not. + + Needs to be an announcement channel! + + + + If set to `true`, the app will automatically publish all new messages from the channel. Otherwise, it will stop automatically publishing messages. + + diff --git a/apps/tako/prisma/migrations/20240519145728_crosspost/migration.sql b/apps/tako/prisma/migrations/20240519145728_crosspost/migration.sql new file mode 100644 index 0000000..bdee62b --- /dev/null +++ b/apps/tako/prisma/migrations/20240519145728_crosspost/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Channel" ADD COLUMN "crosspost" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/tako/prisma/schema.prisma b/apps/tako/prisma/schema.prisma index 97c126e..1619ad8 100644 --- a/apps/tako/prisma/schema.prisma +++ b/apps/tako/prisma/schema.prisma @@ -31,10 +31,11 @@ model Guild { } model Channel { - id String @id @unique - stickyEmbed Boolean @default(false) + id String @id @unique + stickyEmbed Boolean @default(false) stickyMessage String? autoReact String[] @default([]) + crosspost Boolean @default(false) } model Badge { diff --git a/apps/tako/src/commands/utility/crosspost.ts b/apps/tako/src/commands/utility/crosspost.ts new file mode 100644 index 0000000..a0e8cae --- /dev/null +++ b/apps/tako/src/commands/utility/crosspost.ts @@ -0,0 +1,143 @@ +import type { ChatInputCommandInteraction } from 'discord.js'; +import { + ChannelType, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import config from '../../../config.ts'; +import prisma from '../../database.ts'; +import i18next from '../../i18n.ts'; +import { + createEmbed, + getLanguage, + slashCommandTranslator, +} from '../../util/general.ts'; +import type { Command } from '../index.ts'; + +export default { + data: new SlashCommandBuilder() + .setName(i18next.t('crosspost.name', { ns: 'utility' })) + .setNameLocalizations(slashCommandTranslator('crosspost.name', 'utility')) + .setDescription(i18next.t('crosspost.description', { ns: 'utility' })) + .setDescriptionLocalizations( + slashCommandTranslator('crosspost.description', 'utility'), + ) + .addChannelOption((option) => + option + .setName(i18next.t('crosspost.options.channel.name', { ns: 'utility' })) + .setNameLocalizations( + slashCommandTranslator('crosspost.options.channel.name', 'utility'), + ) + .setDescription( + i18next.t('crosspost.options.channel.description', { ns: 'utility' }), + ) + .setDescriptionLocalizations( + slashCommandTranslator( + 'crosspost.options.channel.description', + 'utility', + ), + ) + .addChannelTypes(ChannelType.GuildAnnouncement), + ) + .addBooleanOption((option) => + option + .setName(i18next.t('crosspost.options.state.name', { ns: 'utility' })) + .setNameLocalizations( + slashCommandTranslator('crosspost.options.state.name', 'utility'), + ) + .setDescription( + i18next.t('crosspost.options.state.description', { ns: 'utility' }), + ) + .setDescriptionLocalizations( + slashCommandTranslator( + 'crosspost.options.state.description', + 'utility', + ), + ), + ) + .setDefaultMemberPermissions( + PermissionFlagsBits.ManageChannels | PermissionFlagsBits.ManageMessages, + ) + .setDMPermission(false) + .toJSON(), + async execute(interaction: ChatInputCommandInteraction) { + const channel = + interaction.options.getChannel('channel') ?? interaction.channel; + const existingData = await prisma.channel.findFirst({ + where: { id: channel?.id }, + select: { crosspost: true }, + }); + const state = + interaction.options.getBoolean('state') ?? + !existingData?.crosspost ?? + true; + const lng = await getLanguage( + interaction.guildId, + interaction.user.id, + true, + ); + + if (channel?.type !== ChannelType.GuildAnnouncement) { + await interaction.reply({ + embeds: [ + createEmbed({ + color: config.colors.red, + description: i18next.t( + 'crosspost.errors.invalidChannel.description', + { + ns: 'utility', + lng, + }, + ), + emoji: config.emojis.error, + title: i18next.t('crosspost.errors.invalidChannel.title', { + ns: 'utility', + lng, + }), + }), + ], + ephemeral: true, + }); + return; + } + + await prisma.channel.upsert({ + where: { id: channel.id }, + create: { + id: channel.id, + crosspost: state, + }, + update: { + crosspost: state, + }, + }); + + await interaction.reply({ + embeds: [ + createEmbed({ + color: state ? config.colors.green : config.colors.red, + description: i18next.t( + 'crosspost.success' + + (state ? '.enabled' : '.disabled') + + '.description', + { + ns: 'utility', + lng, + channel: channel.id, + }, + ), + emoji: state ? config.emojis.success : config.emojis.error, + title: i18next.t( + 'crosspost.success' + (state ? '.enabled' : '.disabled') + '.title', + { + ns: 'utility', + lng, + channel: channel.id, + }, + ), + }), + ], + ephemeral: true, + }); + }, +} satisfies Command; diff --git a/apps/tako/src/events/messageCreate.ts b/apps/tako/src/events/messageCreate.ts index c547d7b..d6e4242 100644 --- a/apps/tako/src/events/messageCreate.ts +++ b/apps/tako/src/events/messageCreate.ts @@ -5,6 +5,19 @@ import type { Event } from './index.ts'; export default { name: Events.MessageCreate, async execute(message) { + // Crosspost + if (message.channel.type === ChannelType.GuildAnnouncement) { + const data = await prisma.channel.findFirst({ + where: { id: message.channelId }, + select: { crosspost: true }, + }); + + if (data?.crosspost) { + await message.crosspost(); + } + } + + // Auto-react const isForum = message.channel.isThread() && (message.channel.parent?.type === ChannelType.GuildForum || diff --git a/apps/tako/src/locales/en/utility.json b/apps/tako/src/locales/en/utility.json index bd136f1..764e676 100644 --- a/apps/tako/src/locales/en/utility.json +++ b/apps/tako/src/locales/en/utility.json @@ -202,5 +202,35 @@ "description": "Whether or not to send the translation as an ephemeral message" } } + }, + "crosspost": { + "name": "crosspost", + "description": "Automatically publish all messages in an announcement channel", + "options": { + "channel": { + "name": "channel", + "description": "The channel to publish all messages from" + }, + "state": { + "name": "state", + "description": "Whether or not to enable crossposting" + } + }, + "success": { + "enabled": { + "title": "Enabled Crossposting", + "description": "All new messages from <#{{channel}}> will now be automatically published." + }, + "disabled": { + "title": "Disabled Crossposting", + "description": "All new messages in <#{{channel}}> will no longer be automatically published." + } + }, + "errors": { + "invalidChannel": { + "title": "Invalid Channel", + "description": "The channel you provided is not an announcement channel. Please provide a valid announcement channel!" + } + } } }