Files
fx-test/fluxer_app/src/utils/CommandUtils.tsx
Hampus Kraft 2f557eda8c initial commit
2026-01-01 21:05:54 +00:00

282 lines
7.4 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import * as GuildActionCreators from '~/actions/GuildActionCreators';
import * as GuildMemberActionCreators from '~/actions/GuildMemberActionCreators';
import * as MessageActionCreators from '~/actions/MessageActionCreators';
import * as PrivateChannelActionCreators from '~/actions/PrivateChannelActionCreators';
import {FLUXERBOT_ID, MessageStates, MessageTypes} from '~/Constants';
import {MessageRecord} from '~/records/MessageRecord';
import {UserRecord} from '~/records/UserRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import UserStore from '~/stores/UserStore';
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
const USER_MENTION_REGEX = /<@!?(\d+)>/;
type ParsedCommand =
| {type: 'nick'; nickname: string}
| {type: 'kick'; userId: string; reason?: string}
| {type: 'ban'; userId: string; deleteMessageDays: number; duration: number; reason?: string}
| {type: 'msg'; userId: string; message: string}
| {type: 'me'; content: string}
| {type: 'spoiler'; content: string}
| {type: 'tts'; content: string}
| {type: 'unknown'};
export function parseCommand(content: string): ParsedCommand {
const trimmed = content.trim();
if (trimmed.startsWith('/nick ')) {
const nickname = trimmed.slice(6).trim();
return {type: 'nick', nickname};
}
if (trimmed.startsWith('/kick ')) {
const rest = trimmed.slice(6).trim();
const userMatch = rest.match(USER_MENTION_REGEX);
if (!userMatch) {
return {type: 'unknown'};
}
const userId = userMatch[1];
const afterMention = rest.slice(userMatch[0].length).trim();
const reason = afterMention || undefined;
return {type: 'kick', userId, reason};
}
if (trimmed.startsWith('/ban ')) {
const rest = trimmed.slice(5).trim();
const userMatch = rest.match(USER_MENTION_REGEX);
if (!userMatch) {
return {type: 'unknown'};
}
const userId = userMatch[1];
const afterMention = rest.slice(userMatch[0].length).trim();
// TODO: Parse these from the command
const deleteMessageDays = 1;
const duration = 0;
const reason = afterMention || undefined;
return {type: 'ban', userId, deleteMessageDays, duration, reason};
}
if (trimmed.startsWith('/msg ')) {
const rest = trimmed.slice(5).trim();
const userMatch = rest.match(USER_MENTION_REGEX);
if (!userMatch) {
return {type: 'unknown'};
}
const userId = userMatch[1];
const message = rest.slice(userMatch[0].length).trim();
if (!message) {
return {type: 'unknown'};
}
return {type: 'msg', userId, message};
}
if (trimmed.startsWith('/me ')) {
const content = trimmed.slice(4).trim();
if (!content) {
return {type: 'unknown'};
}
return {type: 'me', content};
}
if (trimmed.startsWith('/spoiler ')) {
const content = trimmed.slice(9).trim();
if (!content) {
return {type: 'unknown'};
}
return {type: 'spoiler', content};
}
if (trimmed.startsWith('/tts ')) {
const content = trimmed.slice(5).trim();
if (!content) {
return {type: 'unknown'};
}
return {type: 'tts', content};
}
return {type: 'unknown'};
}
export function transformWrappingCommands(content: string): string {
const trimmed = content.trim();
if (trimmed.startsWith('/me ')) {
const messageContent = trimmed.slice(4).trim();
if (messageContent) {
return `_${messageContent}_`;
}
}
if (trimmed.startsWith('/spoiler ')) {
const messageContent = trimmed.slice(9).trim();
if (messageContent) {
return `||${messageContent}||`;
}
}
return content;
}
export function isCommand(content: string): boolean {
const trimmed = content.trim();
return (
trimmed.startsWith('/nick ') ||
trimmed.startsWith('/kick ') ||
trimmed.startsWith('/ban ') ||
trimmed.startsWith('/msg ') ||
trimmed.startsWith('/me ') ||
trimmed.startsWith('/spoiler ') ||
trimmed.startsWith('/tts ') ||
(trimmed.startsWith('_') && trimmed.endsWith('_') && trimmed.length > 2)
);
}
export function createSystemMessage(channelId: string, content: string): MessageRecord {
const fluxerbotUser = new UserRecord({
id: FLUXERBOT_ID,
username: 'Fluxerbot',
discriminator: '0000',
avatar: null,
bot: true,
system: true,
flags: 0,
});
const nonce = SnowflakeUtils.fromTimestamp(Date.now());
return new MessageRecord({
id: nonce,
channel_id: channelId,
author: fluxerbotUser.toJSON(),
type: MessageTypes.CLIENT_SYSTEM,
flags: 0,
pinned: false,
mention_everyone: false,
content,
timestamp: new Date().toISOString(),
state: MessageStates.SENT,
nonce,
attachments: [],
});
}
export async function executeCommand(command: ParsedCommand, channelId: string, guildId?: string): Promise<void> {
const currentUserId = AuthenticationStore.currentUserId;
switch (command.type) {
case 'nick': {
if (!guildId) {
throw new Error('Cannot change nickname outside of a guild');
}
const currentMember = GuildMemberStore.getMember(guildId, currentUserId);
const prevNickname = currentMember?.nick || UserStore.getCurrentUser()?.username || 'Unknown';
const newNickname = command.nickname || UserStore.getCurrentUser()?.username || 'Unknown';
await GuildMemberActionCreators.updateProfile(guildId, {
nick: command.nickname || null,
});
const systemMessage = createSystemMessage(
channelId,
`You changed your nickname in this community from **${prevNickname}** to **${newNickname}**.`,
);
MessageActionCreators.createOptimistic(channelId, systemMessage.toJSON());
break;
}
case 'kick': {
if (!guildId) {
throw new Error('Cannot kick members outside of a guild');
}
await GuildMemberActionCreators.kick(guildId, command.userId);
break;
}
case 'ban': {
if (!guildId) {
throw new Error('Cannot ban members outside of a guild');
}
await GuildActionCreators.banMember(
guildId,
command.userId,
command.deleteMessageDays,
command.reason,
command.duration,
);
break;
}
case 'msg': {
try {
const dmChannelId = await PrivateChannelActionCreators.ensureDMChannel(command.userId);
await MessageActionCreators.send(dmChannelId, {
content: command.message,
nonce: SnowflakeUtils.fromTimestamp(Date.now()),
hasAttachments: false,
flags: 0,
});
await PrivateChannelActionCreators.openDMChannel(command.userId);
} catch (_error) {
const user = UserStore.getUser(command.userId);
const username = user?.username || 'user';
const systemMessage = createSystemMessage(
channelId,
`Failed to send a message to **${username}**. They may have DMs disabled or you may be blocked.`,
);
MessageActionCreators.createOptimistic(channelId, systemMessage.toJSON());
}
break;
}
case 'me': {
break;
}
case 'spoiler': {
break;
}
default:
break;
}
}