mirror of
https://github.com/immich-app/discord-bot.git
synced 2026-09-30 13:32:52 +08:00
feat: mattermost modals (#621)
This commit is contained in:
@@ -3,7 +3,7 @@ import type { EmitterWebhookEvent } from '@octokit/webhooks';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { GithubStatusComponent, GithubStatusIncident, StripeBase } from 'src/dtos/webhook.dto';
|
import { GithubStatusComponent, GithubStatusIncident, StripeBase } from 'src/dtos/webhook.dto';
|
||||||
import { FourthwallOrderCreateWebhook, FourthwallOrderUpdateWebhook } from 'src/interfaces/fourthwall.interface';
|
import { FourthwallOrderCreateWebhook, FourthwallOrderUpdateWebhook } from 'src/interfaces/fourthwall.interface';
|
||||||
import { CommandWebhookRequest } from 'src/interfaces/mattermost.interface';
|
import { CommandWebhookRequest, DialogResponse } from 'src/interfaces/mattermost.interface';
|
||||||
import { WebhookService } from 'src/services/webhook.service';
|
import { WebhookService } from 'src/services/webhook.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -54,4 +54,10 @@ export class WebhookController {
|
|||||||
async onMattermostCommand(@Body() dto: CommandWebhookRequest<never>, @Param('slug') slug: string) {
|
async onMattermostCommand(@Body() dto: CommandWebhookRequest<never>, @Param('slug') slug: string) {
|
||||||
return this.service.onMattermostCommand(dto, slug);
|
return this.service.onMattermostCommand(dto, slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('mattermost/dialog/:slug')
|
||||||
|
@HttpCode(200)
|
||||||
|
async onMattermostDialog(@Body() dto: DialogResponse, @Param('slug') slug: string) {
|
||||||
|
return this.service.onMattermostDialog(dto, slug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,13 +609,13 @@ export class DiscordCommands {
|
|||||||
return interaction.respond(results);
|
return interaction.respond(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await this.scheduledMessageService.removeScheduledMessage(name);
|
const message = await this.scheduledMessageService.removeScheduledMessage(name, 'discord');
|
||||||
return interaction.reply(message);
|
return interaction.reply(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Slash({ name: 'schedule-list', description: 'List all scheduled messages' })
|
@Slash({ name: 'schedule-list', description: 'List all scheduled messages' })
|
||||||
async handleScheduleList(interaction: CommandInteraction) {
|
async handleScheduleList(interaction: CommandInteraction) {
|
||||||
const messages = await this.scheduledMessageService.listScheduledMessages();
|
const messages = await this.scheduledMessageService.listScheduledMessages('discord');
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return interaction.reply({ content: 'No scheduled messages found.', flags: [MessageFlags.Ephemeral] });
|
return interaction.reply({ content: 'No scheduled messages found.', flags: [MessageFlags.Ephemeral] });
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export interface IDatabaseRepository {
|
|||||||
getRSSFeeds(channelId?: string): Promise<RSSFeed[]>;
|
getRSSFeeds(channelId?: string): Promise<RSSFeed[]>;
|
||||||
removeRSSFeed(url: string, channelId: string): Promise<void>;
|
removeRSSFeed(url: string, channelId: string): Promise<void>;
|
||||||
updateRSSFeed(entity: UpdateRSSFeed): Promise<void>;
|
updateRSSFeed(entity: UpdateRSSFeed): Promise<void>;
|
||||||
getScheduledMessages(): Promise<ScheduledMessage[]>;
|
getScheduledMessages(service?: 'discord' | 'mattermost'): Promise<ScheduledMessage[]>;
|
||||||
getScheduledMessage(name: string): Promise<ScheduledMessage | undefined>;
|
getScheduledMessage(name: string, service: 'discord' | 'mattermost'): Promise<ScheduledMessage | undefined>;
|
||||||
createScheduledMessage(entity: NewScheduledMessage): Promise<ScheduledMessage>;
|
createScheduledMessage(entity: NewScheduledMessage): Promise<ScheduledMessage>;
|
||||||
updateScheduledMessage(entity: UpdateScheduledMessage & { name: string }): Promise<ScheduledMessage | undefined>;
|
updateScheduledMessage(entity: UpdateScheduledMessage & { name: string }): Promise<ScheduledMessage | undefined>;
|
||||||
removeScheduledMessage(id: string): Promise<void>;
|
removeScheduledMessage(id: string): Promise<void>;
|
||||||
|
|||||||
@@ -30,6 +30,105 @@ export type CommandWebhookRequest<T extends CommandParameters> = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DateConfig = {
|
||||||
|
min_date?: Date | string;
|
||||||
|
max_date?: Date | string;
|
||||||
|
};
|
||||||
|
type DatetimeConfig = DateConfig & {
|
||||||
|
time_interval?: number;
|
||||||
|
location_timezone?: string;
|
||||||
|
manual_time_entry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DialogBaseField = {
|
||||||
|
/* max length 24 */
|
||||||
|
display_name: string;
|
||||||
|
name: string;
|
||||||
|
optional?: boolean;
|
||||||
|
help_text?: string;
|
||||||
|
default?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
type TextBaseField = DialogBaseField & {
|
||||||
|
subtype?: 'text' | 'email' | 'number' | 'password' | 'tel' | 'url';
|
||||||
|
min_length?: number;
|
||||||
|
max_length?: boolean;
|
||||||
|
};
|
||||||
|
type DialogTextField = TextBaseField & { type: 'text' };
|
||||||
|
type DialogTextareaField = TextBaseField & { type: 'textarea' };
|
||||||
|
type DialogSelectField = DialogBaseField & { type: 'select'; multiselect?: boolean; refresh?: boolean } & (
|
||||||
|
| { data_source: 'users' | 'channels' }
|
||||||
|
| { data_source: 'dynamic'; data_source_url: `https://${string}/plugins/${string}` }
|
||||||
|
| { data_source?: undefined; options: Array<{ text: string; value: string }> }
|
||||||
|
);
|
||||||
|
type DialogCheckboxField = DialogBaseField & { type: 'bool' };
|
||||||
|
type DialogRadioField = Omit<DialogBaseField, 'placeholder' | 'optional'> & {
|
||||||
|
type: 'radio';
|
||||||
|
options: Array<{ text: string; value: string }>;
|
||||||
|
};
|
||||||
|
type DialogDateField = DialogBaseField & {
|
||||||
|
type: 'date';
|
||||||
|
datetime_config: DateConfig;
|
||||||
|
};
|
||||||
|
type DialogDatetimeField = DialogBaseField & {
|
||||||
|
type: 'datetime';
|
||||||
|
datetime_config: DatetimeConfig;
|
||||||
|
};
|
||||||
|
type DialogFileField = DialogBaseField & { type: 'file'; allow_multiple?: boolean };
|
||||||
|
type DialogActionButtonField = {
|
||||||
|
display_name: string;
|
||||||
|
name: string;
|
||||||
|
type: 'action_button';
|
||||||
|
action_button: { url: string; context?: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
|
||||||
|
type DialogField =
|
||||||
|
| DialogTextField
|
||||||
|
| DialogTextareaField
|
||||||
|
| DialogSelectField
|
||||||
|
| DialogCheckboxField
|
||||||
|
| DialogRadioField
|
||||||
|
| DialogDateField
|
||||||
|
| DialogDatetimeField
|
||||||
|
| DialogFileField
|
||||||
|
| DialogActionButtonField;
|
||||||
|
|
||||||
|
export type Dialog = {
|
||||||
|
/* max length 24 */
|
||||||
|
title: string;
|
||||||
|
introduction_text?: string;
|
||||||
|
elements: Array<DialogField>;
|
||||||
|
icon_url?: string;
|
||||||
|
submit_label?: string;
|
||||||
|
notify_on_cancel?: boolean;
|
||||||
|
state?: string;
|
||||||
|
source_url?: string;
|
||||||
|
};
|
||||||
|
export type DialogResponse = {
|
||||||
|
type: 'dialog_submission';
|
||||||
|
callback_id: string;
|
||||||
|
state: string;
|
||||||
|
user_id: string;
|
||||||
|
channel_id: string;
|
||||||
|
team_id: string;
|
||||||
|
submission: Record<string, string>;
|
||||||
|
file_ids?: string[];
|
||||||
|
cancelled: boolean;
|
||||||
|
};
|
||||||
|
type ResponseType<T extends Dialog, K> = (T['elements'][number] & { name: K })['type'] extends 'bool'
|
||||||
|
? boolean
|
||||||
|
: string;
|
||||||
|
export type DialogData<T extends Dialog> =
|
||||||
|
| { cancelled: true }
|
||||||
|
| ({ cancelled: false } & {
|
||||||
|
[name in Exclude<T['elements'][number], { type: 'file' } | { optional: true }>['name']]: ResponseType<T, name>;
|
||||||
|
} & {
|
||||||
|
[name in Exclude<T['elements'][number], { type: 'file' }>['name']]?: ResponseType<T, name>;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
} & ('file' extends Exclude<T['elements'][number], { optional: true }>['type'] ? { file_ids: string[] } : {}) &
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
('file' extends T['elements'][number]['type'] ? { file_ids?: string[] } : {}));
|
||||||
|
|
||||||
type ParameterType = 'text' | 'userMention' | 'channelMention' | 'number';
|
type ParameterType = 'text' | 'userMention' | 'channelMention' | 'number';
|
||||||
export type CommandParameters = Array<{ name: string; type: ParameterType; optional: boolean }>;
|
export type CommandParameters = Array<{ name: string; type: ParameterType; optional: boolean }>;
|
||||||
type RequiredProps = 'display_name' | 'description' | 'trigger' | 'team_id';
|
type RequiredProps = 'display_name' | 'description' | 'trigger' | 'team_id';
|
||||||
@@ -58,4 +157,6 @@ export interface IMattermostInterface {
|
|||||||
handler: (data: CommandWebhookRequest<T>) => unknown,
|
handler: (data: CommandWebhookRequest<T>) => unknown,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
runCommand: (id: string, data: CommandWebhookRequest<never>) => Promise<unknown>;
|
runCommand: (id: string, data: CommandWebhookRequest<never>) => Promise<unknown>;
|
||||||
|
openDialog: <const T extends Dialog>(triggerId: string, dialog: T) => Promise<DialogData<T>>;
|
||||||
|
submitDialog: (dto: DialogResponse, slug: string) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,12 +214,21 @@ export class DatabaseRepository implements IDatabaseRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
getScheduledMessages(): Promise<ScheduledMessage[]> {
|
getScheduledMessages(service?: 'discord' | 'mattermost'): Promise<ScheduledMessage[]> {
|
||||||
return this.db.selectFrom('scheduled_message').selectAll().execute();
|
return this.db
|
||||||
|
.selectFrom('scheduled_message')
|
||||||
|
.selectAll()
|
||||||
|
.$if(service !== undefined, (qb) => qb.where('service', '=', service!))
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
getScheduledMessage(name: string): Promise<ScheduledMessage | undefined> {
|
getScheduledMessage(name: string, service: 'discord' | 'mattermost'): Promise<ScheduledMessage | undefined> {
|
||||||
return this.db.selectFrom('scheduled_message').where('name', '=', name).selectAll().executeTakeFirst();
|
return this.db
|
||||||
|
.selectFrom('scheduled_message')
|
||||||
|
.where('name', '=', name)
|
||||||
|
.where('service', '=', service)
|
||||||
|
.selectAll()
|
||||||
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
createScheduledMessage(entity: NewScheduledMessage): Promise<ScheduledMessage> {
|
createScheduledMessage(entity: NewScheduledMessage): Promise<ScheduledMessage> {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
CommandCreate,
|
CommandCreate,
|
||||||
CommandParameters,
|
CommandParameters,
|
||||||
CommandWebhookRequest,
|
CommandWebhookRequest,
|
||||||
|
Dialog,
|
||||||
|
DialogData,
|
||||||
|
DialogResponse,
|
||||||
IMattermostInterface,
|
IMattermostInterface,
|
||||||
MattermostEvents,
|
MattermostEvents,
|
||||||
Post,
|
Post,
|
||||||
@@ -22,6 +25,7 @@ export class MattermostRepository implements IMattermostInterface {
|
|||||||
string,
|
string,
|
||||||
{ parameters?: CommandParameters; handler: (data: CommandWebhookRequest<never>) => unknown }
|
{ parameters?: CommandParameters; handler: (data: CommandWebhookRequest<never>) => unknown }
|
||||||
> = {};
|
> = {};
|
||||||
|
#modalHandlers: Record<string, (data: DialogResponse) => void> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const { mattermost } = getConfig();
|
const { mattermost } = getConfig();
|
||||||
@@ -246,4 +250,37 @@ export class MattermostRepository implements IMattermostInterface {
|
|||||||
async joinChannel(channelId: string) {
|
async joinChannel(channelId: string) {
|
||||||
await this.#client.addToChannel(this.#user.id, channelId);
|
await this.#client.addToChannel(this.#user.id, channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openDialog<const T extends Dialog>(triggerId: string, dialog: T) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const { promise, reject, resolve } = Promise.withResolvers<DialogData<T>>();
|
||||||
|
|
||||||
|
this.#modalHandlers[id] = (response) => {
|
||||||
|
if (response.cancelled) {
|
||||||
|
resolve({ cancelled: true });
|
||||||
|
} else {
|
||||||
|
resolve({ ...response.submission, cancelled: false, file_ids: response.file_ids } as DialogData<T>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${this.#client.getUrl()}/api/v4/actions/dialogs/open`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${this.#client.getToken()}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: `https://discord-webhooks.immich.cloud/webhooks/mattermost/dialog/${id}`,
|
||||||
|
trigger_id: triggerId,
|
||||||
|
dialog: { ...dialog, notify_on_cancel: true },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitDialog(dto: DialogResponse, slug: string) {
|
||||||
|
this.#modalHandlers[slug](dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "scheduled_message" DROP CONSTRAINT "scheduled_message_pkey";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "scheduled_message" ADD CONSTRAINT "scheduled_message_pkey" PRIMARY KEY ("id", "service");`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "scheduled_message" DROP CONSTRAINT "scheduled_message_pkey";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "scheduled_message" ADD CONSTRAINT "scheduled_message_pkey" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools';
|
import { Column, CreateDateColumn, Generated, GeneratedColumn, Table } from '@immich/sql-tools';
|
||||||
|
|
||||||
@Table('scheduled_message')
|
@Table('scheduled_message')
|
||||||
export class ScheduledMessageTable {
|
export class ScheduledMessageTable {
|
||||||
@PrimaryGeneratedColumn()
|
@GeneratedColumn({ primary: true })
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
@@ -23,7 +23,7 @@ export class ScheduledMessageTable {
|
|||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Column({ default: 'discord' })
|
@Column({ default: 'discord', primary: true })
|
||||||
service!: 'discord' | 'mattermost';
|
service!: 'discord' | 'mattermost';
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ const newMattermostMockRepository = (): Mocked<IMattermostInterface> => ({
|
|||||||
joinChannel: vitest.fn(),
|
joinChannel: vitest.fn(),
|
||||||
registerCommand: vitest.fn() as any,
|
registerCommand: vitest.fn() as any,
|
||||||
runCommand: vitest.fn(),
|
runCommand: vitest.fn(),
|
||||||
|
openDialog: vitest.fn(),
|
||||||
|
submitDialog: vitest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const newFourthwallMockRepository = (): Mocked<IFourthwallRepository> => ({
|
const newFourthwallMockRepository = (): Mocked<IFourthwallRepository> => ({
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ const newMattermostMock = (): Mocked<IMattermostInterface> => ({
|
|||||||
send: vitest.fn(),
|
send: vitest.fn(),
|
||||||
streamChannels: vitest.fn(),
|
streamChannels: vitest.fn(),
|
||||||
updatePost: vitest.fn(),
|
updatePost: vitest.fn(),
|
||||||
|
openDialog: vitest.fn(),
|
||||||
|
submitDialog: vitest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeScheduledMessage = (overrides: Partial<ScheduledMessage> = {}): ScheduledMessage => ({
|
const makeScheduledMessage = (overrides: Partial<ScheduledMessage> = {}): ScheduledMessage => ({
|
||||||
@@ -117,7 +119,7 @@ describe('ScheduledMessageService', () => {
|
|||||||
it('should return not-found message when name does not exist', async () => {
|
it('should return not-found message when name does not exist', async () => {
|
||||||
databaseMock.getScheduledMessage.mockResolvedValue(undefined);
|
databaseMock.getScheduledMessage.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const result = await sut.removeScheduledMessage('nonexistent');
|
const result = await sut.removeScheduledMessage('nonexistent', 'discord');
|
||||||
|
|
||||||
expect(result).toEqual('Scheduled message not found');
|
expect(result).toEqual('Scheduled message not found');
|
||||||
expect(databaseMock.removeScheduledMessage).not.toHaveBeenCalled();
|
expect(databaseMock.removeScheduledMessage).not.toHaveBeenCalled();
|
||||||
@@ -127,7 +129,7 @@ describe('ScheduledMessageService', () => {
|
|||||||
const msg = makeScheduledMessage({ id: 'rm-1', name: 'to-remove' });
|
const msg = makeScheduledMessage({ id: 'rm-1', name: 'to-remove' });
|
||||||
databaseMock.getScheduledMessage.mockResolvedValue(msg);
|
databaseMock.getScheduledMessage.mockResolvedValue(msg);
|
||||||
|
|
||||||
const result = await sut.removeScheduledMessage('to-remove');
|
const result = await sut.removeScheduledMessage('to-remove', 'mattermost');
|
||||||
|
|
||||||
expect(databaseMock.removeScheduledMessage).toHaveBeenCalledWith('rm-1');
|
expect(databaseMock.removeScheduledMessage).toHaveBeenCalledWith('rm-1');
|
||||||
expect(result).toEqual('Removed scheduled message `to-remove`');
|
expect(result).toEqual('Removed scheduled message `to-remove`');
|
||||||
@@ -189,7 +191,7 @@ describe('ScheduledMessageService', () => {
|
|||||||
const messages = [makeScheduledMessage({ channelId: 'ch-1' }), makeScheduledMessage({ channelId: 'ch-2' })];
|
const messages = [makeScheduledMessage({ channelId: 'ch-1' }), makeScheduledMessage({ channelId: 'ch-2' })];
|
||||||
databaseMock.getScheduledMessages.mockResolvedValue(messages);
|
databaseMock.getScheduledMessages.mockResolvedValue(messages);
|
||||||
|
|
||||||
const result = await sut.listScheduledMessages();
|
const result = await sut.listScheduledMessages('discord');
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,13 +83,85 @@ export class ScheduledMessageService {
|
|||||||
parameters: [{ name: 'name', type: 'text', optional: false }],
|
parameters: [{ name: 'name', type: 'text', optional: false }],
|
||||||
},
|
},
|
||||||
async ({ parameters: { name } }) => {
|
async ({ parameters: { name } }) => {
|
||||||
const message = await this.removeScheduledMessage(name);
|
const message = await this.removeScheduledMessage(name, 'mattermost');
|
||||||
return {
|
return {
|
||||||
response_type: 'in_channel',
|
response_type: 'in_channel',
|
||||||
text: message,
|
text: message,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.mattermost.registerCommand(
|
||||||
|
{
|
||||||
|
trigger: 'schedule-list',
|
||||||
|
display_name: 'Schedule lists',
|
||||||
|
description: 'List all scheduled messages',
|
||||||
|
auto_complete: true,
|
||||||
|
team_id: Constants.Mattermost.Teams.Immich,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const messages = await this.listScheduledMessages('mattermost');
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return { text: 'No scheduled messages found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: messages
|
||||||
|
.map(
|
||||||
|
(message) =>
|
||||||
|
`- **${message.name}**: ${inlineCode(message.cronExpression)} in ~${message.channelId}: ${message.message}`,
|
||||||
|
)
|
||||||
|
.join('\n'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.mattermost.registerCommand(
|
||||||
|
{
|
||||||
|
trigger: 'schedule-edit',
|
||||||
|
display_name: 'Edit schedule',
|
||||||
|
description: 'Edit an existing scheduled message',
|
||||||
|
team_id: Constants.Mattermost.Teams.Immich,
|
||||||
|
auto_complete: true,
|
||||||
|
auto_complete_desc: 'The name of the scheduled message to edit',
|
||||||
|
parameters: [{ name: 'name', type: 'text', optional: false }],
|
||||||
|
},
|
||||||
|
async ({ trigger_id, parameters: { name } }) => {
|
||||||
|
const message = await this.database.getScheduledMessage(name, 'mattermost');
|
||||||
|
if (!message) {
|
||||||
|
return 'Scheduled message not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const response = await this.mattermost.openDialog(trigger_id, {
|
||||||
|
title: message.name,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
display_name: 'Cron expression',
|
||||||
|
name: 'cronExpression',
|
||||||
|
default: message.cronExpression,
|
||||||
|
},
|
||||||
|
{ type: 'textarea', display_name: 'Message', name: 'message', default: message.message },
|
||||||
|
{
|
||||||
|
type: 'bool',
|
||||||
|
display_name: 'Suppress embeds',
|
||||||
|
name: 'suppressEmbeds',
|
||||||
|
optional: true,
|
||||||
|
default: String(message.suppressEmbeds),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.database.updateScheduledMessage({ name, ...response });
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerJob({
|
private registerJob({
|
||||||
@@ -144,7 +216,7 @@ export class ScheduledMessageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async editScheduledMessage(name: string) {
|
async editScheduledMessage(name: string) {
|
||||||
const message = await this.database.getScheduledMessage(name);
|
const message = await this.database.getScheduledMessage(name, 'discord');
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return 'Scheduled message not found';
|
return 'Scheduled message not found';
|
||||||
}
|
}
|
||||||
@@ -202,8 +274,8 @@ export class ScheduledMessageService {
|
|||||||
await interaction.reply(`Successfully updated scheduled message ${inlineCode(updatedMessage.name)}`);
|
await interaction.reply(`Successfully updated scheduled message ${inlineCode(updatedMessage.name)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeScheduledMessage(name: string) {
|
async removeScheduledMessage(name: string, service: 'discord' | 'mattermost') {
|
||||||
const message = await this.database.getScheduledMessage(name);
|
const message = await this.database.getScheduledMessage(name, service);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return 'Scheduled message not found';
|
return 'Scheduled message not found';
|
||||||
}
|
}
|
||||||
@@ -219,7 +291,7 @@ export class ScheduledMessageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getScheduledMessages(value?: string) {
|
async getScheduledMessages(value?: string) {
|
||||||
let messages = await this.database.getScheduledMessages();
|
let messages = await this.database.getScheduledMessages('discord');
|
||||||
if (value) {
|
if (value) {
|
||||||
const query = value.toLowerCase();
|
const query = value.toLowerCase();
|
||||||
messages = messages.filter(({ name }) => name.toLowerCase().includes(query));
|
messages = messages.filter(({ name }) => name.toLowerCase().includes(query));
|
||||||
@@ -233,7 +305,7 @@ export class ScheduledMessageService {
|
|||||||
.slice(0, 25);
|
.slice(0, 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listScheduledMessages() {
|
async listScheduledMessages(service: 'discord' | 'mattermost') {
|
||||||
return this.database.getScheduledMessages();
|
return this.database.getScheduledMessages(service);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
IFourthwallRepository,
|
IFourthwallRepository,
|
||||||
} from 'src/interfaces/fourthwall.interface';
|
} from 'src/interfaces/fourthwall.interface';
|
||||||
import { IGithubInterface } from 'src/interfaces/github.interface';
|
import { IGithubInterface } from 'src/interfaces/github.interface';
|
||||||
import { CommandWebhookRequest, IMattermostInterface } from 'src/interfaces/mattermost.interface';
|
import { CommandWebhookRequest, DialogResponse, IMattermostInterface } from 'src/interfaces/mattermost.interface';
|
||||||
import { IOutlineInterface } from 'src/interfaces/outline.interface';
|
import { IOutlineInterface } from 'src/interfaces/outline.interface';
|
||||||
import { IZulipInterface } from 'src/interfaces/zulip.interface';
|
import { IZulipInterface } from 'src/interfaces/zulip.interface';
|
||||||
import { FourthwallRepository } from 'src/repositories/fourthwall.repository';
|
import { FourthwallRepository } from 'src/repositories/fourthwall.repository';
|
||||||
@@ -244,6 +244,10 @@ export class WebhookService {
|
|||||||
return this.mattermost.runCommand(slug, dto);
|
return this.mattermost.runCommand(slug, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onMattermostDialog(dto: DialogResponse, slug: string) {
|
||||||
|
return this.mattermost.submitDialog(dto, slug);
|
||||||
|
}
|
||||||
|
|
||||||
private async handleFourthwallOrder(dto: FourthwallOrderCreateWebhook | FourthwallOrderUpdateWebhook) {
|
private async handleFourthwallOrder(dto: FourthwallOrderCreateWebhook | FourthwallOrderUpdateWebhook) {
|
||||||
const { fourthwall } = getConfig();
|
const { fourthwall } = getConfig();
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "node20",
|
"module": "nodenext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"target": "es2022",
|
"target": "esnext",
|
||||||
"moduleResolution": "node16",
|
"moduleResolution": "nodenext",
|
||||||
"lib": ["dom", "es2023"],
|
"lib": ["dom", "ESNext"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
|||||||
Reference in New Issue
Block a user