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!"
+ }
+ }
}
}