diff --git a/scripts/wichtelbot/endpoint/definitions/additions.ts b/scripts/wichtelbot/endpoint/definitions/additions.ts new file mode 100644 index 0000000000000000000000000000000000000000..99cbb122e3d2c496c0360e5eb544eafd96060ed0 --- /dev/null +++ b/scripts/wichtelbot/endpoint/definitions/additions.ts @@ -0,0 +1,13 @@ +import { Component } from "./component/component"; +import { Visualisation } from "./visualisation/visualisation"; + +/** Additions to a text message. \ + * If Visualisation[]: \ + * - Provide a finer control for how the client library may present the message. + * If Component[]: \ + * - Components to send with the message. The client library decides if and how to present these. + * If string: \ + * - An optional URL to an image. The client library must decide how it uses this information. +* It can show the image directly, attach it to the message, send it separately or simply send the URL (if nothing else is possible). +*/ +export type Additions = Visualisation[] | Component[] | string; diff --git a/scripts/wichtelbot/endpoint/definitions/channel.ts b/scripts/wichtelbot/endpoint/definitions/channel.ts index 1ce07a304b0535bb6aa4153c152cf53d9cc7bc63..632597d3b752a2da03824270d380960d2b4e4dc5 100644 --- a/scripts/wichtelbot/endpoint/definitions/channel.ts +++ b/scripts/wichtelbot/endpoint/definitions/channel.ts @@ -1,5 +1,5 @@ +import { Additions } from "."; import { ChannelType } from "./channelType"; -import { Component } from "./component/component"; /** * Communication happens in channels, messages are sent to channels, users interact in channels. @@ -17,10 +17,7 @@ export interface Channel /** * A method to send a message to the channel. * @param text The text to send. - * @param components An optional list of components to send with the message. The client library decides if and how to present these. - * @param imageUrl An optional URL to an image. The client library must decide how it - * uses this information. It can show the image directly, attach it to the message, - * send it separately or simply send the URL (if nothing else is possible). + * @param additions Optional additions. */ - send (text: string, components?: Component[], imageUrl?: string): Promise<void>; + send (text: string, additions?: Additions): Promise<void>; } diff --git a/scripts/wichtelbot/endpoint/definitions/index.ts b/scripts/wichtelbot/endpoint/definitions/index.ts index d33af63c33a09a7823c0471c839cdc6329a87e25..65bbda30cce15fcef60491e5c9a79a0d7707c847 100644 --- a/scripts/wichtelbot/endpoint/definitions/index.ts +++ b/scripts/wichtelbot/endpoint/definitions/index.ts @@ -5,6 +5,9 @@ export { ButtonStyle } from './component/buttonStyle'; export { Component } from './component/component'; export { ComponentType } from './component/componentType'; export { Select } from './component/select'; +export { Additions } from './additions'; +export { Visualisation } from './visualisation/visualisation'; +export { VisualisationType } from './visualisation/visualisationType'; export { default as Client } from './client'; export { default as Message } from './message'; export { default as State } from './state'; diff --git a/scripts/wichtelbot/endpoint/definitions/message.ts b/scripts/wichtelbot/endpoint/definitions/message.ts index a837e77eb38f007fa3ffa0a27a55797534c9587e..1d75affbb455ad8a63065a764a458bdd3c339392 100644 --- a/scripts/wichtelbot/endpoint/definitions/message.ts +++ b/scripts/wichtelbot/endpoint/definitions/message.ts @@ -1,6 +1,6 @@ +import { Additions } from "./additions"; import { Channel } from "./channel"; import Client from './client'; -import { Component } from "./component/component"; import User from "./user"; /** @@ -47,11 +47,9 @@ export default interface Message * How this is exactly represented (in the same channel, as a tree, with a mention or with a special connection) is free to be chosen * by the client library. * @param text The text to send. - * @param components An optional list of components to send with the message. The client library decides if and how to present these. - * @param imageUrl An optional URL to an image. The client library must decide how it uses this information. It can show the image - * directly, attach it to the message, send it separately or simply send the URL (if nothing else is possible). + * @param additions Optional additions. */ - reply (text: string, components?: Component[], imageUrl?: string): Promise<void>; + reply (text: string, additions?: Additions): Promise<void>; /** * A method to parse the message. \ * Parsing extracts command and parameters. diff --git a/scripts/wichtelbot/endpoint/definitions/user.ts b/scripts/wichtelbot/endpoint/definitions/user.ts index d5304692cd9142a1423ee688bb9898a5dcad05ab..8459a843dcb8b417f30672876d49ccc620848836 100644 --- a/scripts/wichtelbot/endpoint/definitions/user.ts +++ b/scripts/wichtelbot/endpoint/definitions/user.ts @@ -1,4 +1,4 @@ -import { Component } from "./component/component"; +import { Additions } from "./additions"; /** * Interface representation of a user. @@ -28,10 +28,7 @@ export default interface User * How this is exactly represented (with a direct message or as a mention or similar) * is free to be chosen by the client library. * @param text The text to send. - * @param components An optional list of components to send with the message. The client library decides if and how to present these. - * @param imageUrl An optional URL to an image. The client library must decide how it - * uses this information. It can show the image directly, attach it to the message, - * send it separately or simply send the URL (if nothing else is possible). + * @param additions Optional additions. */ - send (text: string, components?: Component[], imageUrl?: string): Promise<void>; + send (text: string, additions?: Additions): Promise<void>; } diff --git a/scripts/wichtelbot/endpoint/definitions/visualisation/visualisation.ts b/scripts/wichtelbot/endpoint/definitions/visualisation/visualisation.ts new file mode 100644 index 0000000000000000000000000000000000000000..436dfadde09d56a60082e68be81c0ad8ed613d5b --- /dev/null +++ b/scripts/wichtelbot/endpoint/definitions/visualisation/visualisation.ts @@ -0,0 +1,8 @@ +import { VisualisationType } from "./visualisationType"; + +export interface Visualisation +{ + headline: string; + text: string; + type: VisualisationType; +} diff --git a/scripts/wichtelbot/endpoint/definitions/visualisation/visualisationType.ts b/scripts/wichtelbot/endpoint/definitions/visualisation/visualisationType.ts new file mode 100644 index 0000000000000000000000000000000000000000..05d0ac2c5bafd7fa19b200c2b859cfb3d2859abd --- /dev/null +++ b/scripts/wichtelbot/endpoint/definitions/visualisation/visualisationType.ts @@ -0,0 +1,5 @@ +export enum VisualisationType +{ + Compact = "compact", + Normal = "normal", +} diff --git a/scripts/wichtelbot/endpoint/implementations/discord/discordChannel.ts b/scripts/wichtelbot/endpoint/implementations/discord/discordChannel.ts index c58de948a6189b8c8ac9893ceb3bdab8660ccf58..0a245fd817f12695c4868cb720e2297918c32aef 100644 --- a/scripts/wichtelbot/endpoint/implementations/discord/discordChannel.ts +++ b/scripts/wichtelbot/endpoint/implementations/discord/discordChannel.ts @@ -1,5 +1,5 @@ import * as Discord from 'discord.js'; -import { Channel, ChannelType, Component } from '../../definitions'; +import { Additions, Channel, ChannelType } from '../../definitions'; import { DiscordUtils } from './discordUtils'; import Utils from '../../../../utility/utils'; @@ -48,7 +48,7 @@ export class DiscordChannel implements Channel return this.channel.id; } - public async send (text: string, components?: Component[], imageUrl?: string): Promise<void> + public async send (text: string, additions?: Additions): Promise<void> { if (this.channel === null) { @@ -57,6 +57,6 @@ export class DiscordChannel implements Channel const splittetText = Utils.splitTextNaturally(text, DiscordUtils.maxMessageLength); - await DiscordUtils.sendMultiMessage(this.channel.send.bind(this.channel), splittetText, components, imageUrl); + await DiscordUtils.sendMultiMessage(this.channel.send.bind(this.channel), splittetText, additions); } } diff --git a/scripts/wichtelbot/endpoint/implementations/discord/discordInteraction.ts b/scripts/wichtelbot/endpoint/implementations/discord/discordInteraction.ts index 5950169192b94a46455bdabc9d7972bfa8c1266d..25b43604858c8cff04996e186565ce0a0523e947 100644 --- a/scripts/wichtelbot/endpoint/implementations/discord/discordInteraction.ts +++ b/scripts/wichtelbot/endpoint/implementations/discord/discordInteraction.ts @@ -1,5 +1,5 @@ import * as Discord from 'discord.js'; -import { Component, Message } from '../../definitions'; +import { Additions, Message } from '../../definitions'; import { DiscordUtils, SendMessage } from './discordUtils'; import Config from '../../../../utility/config'; import { DiscordChannel } from './discordChannel'; @@ -104,7 +104,7 @@ export class DiscordInteraction extends MessageWithParser implements Message } } - public async reply (text: string, components?: Component[], imageUrl?: string): Promise<void> + public async reply (text: string, additions?: Additions): Promise<void> { const splittetText = Utils.splitTextNaturally(text, DiscordUtils.maxMessageWithMentionLength); @@ -150,12 +150,12 @@ export class DiscordInteraction extends MessageWithParser implements Message sendMessageFunction = this.interaction.followUp.bind(this.interaction); } - await DiscordUtils.sendMultiMessage(sendMessageFunction, splittetText, components, imageUrl); + await DiscordUtils.sendMultiMessage(sendMessageFunction, splittetText, additions); } else if (this.interaction.isCommand() || this.interaction.isContextMenu()) { - await DiscordUtils.sendMultiMessage(this.interaction.editReply.bind(this.interaction), splittetText, components, imageUrl); + await DiscordUtils.sendMultiMessage(this.interaction.editReply.bind(this.interaction), splittetText, additions); } else { diff --git a/scripts/wichtelbot/endpoint/implementations/discord/discordMessage.ts b/scripts/wichtelbot/endpoint/implementations/discord/discordMessage.ts index 2a2c2d418f7a58e1d4e96c645f7166c0d2bfe15f..9025cd56c0a144e0bb4569483a938ce8607a4340 100644 --- a/scripts/wichtelbot/endpoint/implementations/discord/discordMessage.ts +++ b/scripts/wichtelbot/endpoint/implementations/discord/discordMessage.ts @@ -1,5 +1,5 @@ import * as Discord from 'discord.js'; -import { Component, Message } from '../../definitions'; +import { Additions, Message } from '../../definitions'; import { DiscordChannel } from './discordChannel'; import { DiscordClient } from './discordClient'; import { DiscordUser } from './discordUser'; @@ -50,16 +50,10 @@ export class DiscordMessage extends MessageWithParser implements Message return this.responsibleClient; } - public async reply (text: string, components?: Component[], imageUrl?: string): Promise<void> + public async reply (text: string, additions?: Additions): Promise<void> { const splittetText = Utils.splitTextNaturally(text, DiscordUtils.maxMessageWithMentionLength); - await DiscordUtils.sendMultiMessage(this.message.channel.send.bind(this.message.channel), splittetText, components, imageUrl); - - /* TODO: This is really only needed for the Steckbrief. - Instead of naively splitting the text, we could create an embed for each part. It has a title for the question of the - Steckbrief and a description for the answer with a length limit of 4096 (much more than the 2000 of a message). - -> NO! This is also used for the "sendCurrentXY" functions. Maybe we need to standardise this like the components? - */ + await DiscordUtils.sendMultiMessage(this.message.channel.send.bind(this.message.channel), splittetText, additions); } } diff --git a/scripts/wichtelbot/endpoint/implementations/discord/discordUser.ts b/scripts/wichtelbot/endpoint/implementations/discord/discordUser.ts index d6d19e8262204305c725bbe590e092dd019f9344..74e4cd7f84cbe7371ec8dd04f8db55d7aae68b81 100644 --- a/scripts/wichtelbot/endpoint/implementations/discord/discordUser.ts +++ b/scripts/wichtelbot/endpoint/implementations/discord/discordUser.ts @@ -1,5 +1,5 @@ import * as Discord from 'discord.js'; -import { Component, User } from '../../definitions'; +import { Additions, User } from '../../definitions'; import { DiscordUtils } from './discordUtils'; import Utils from '../../../../utility/utils'; @@ -32,10 +32,10 @@ export class DiscordUser implements User return this.user.bot; } - public async send (text: string, components?: Component[], imageUrl?: string): Promise<void> + public async send (text: string, additions: Additions): Promise<void> { const splittetText = Utils.splitTextNaturally(text, DiscordUtils.maxMessageLength); - await DiscordUtils.sendMultiMessage(this.user.send.bind(this.user), splittetText, components, imageUrl); + await DiscordUtils.sendMultiMessage(this.user.send.bind(this.user), splittetText, additions); } } diff --git a/scripts/wichtelbot/endpoint/implementations/discord/discordUtils.ts b/scripts/wichtelbot/endpoint/implementations/discord/discordUtils.ts index ab8e482439c5a08996ae8edd3be4183e80209d37..7f7a003b9de2332891b9cb13a506e4162fdae0b7 100644 --- a/scripts/wichtelbot/endpoint/implementations/discord/discordUtils.ts +++ b/scripts/wichtelbot/endpoint/implementations/discord/discordUtils.ts @@ -1,11 +1,12 @@ import * as Discord from 'discord.js'; -import { ButtonStyle, Component, ComponentType } from '../../definitions'; +import { Additions, ButtonStyle, Component, ComponentType, Visualisation, VisualisationType } from '../../definitions'; const safetyMargin = 16; const maxMessageLength = 2000 - safetyMargin; const maxUserNameLength = 32; // Alternatively maxUserIdLength = 20 should be enough, but this is safe. const maxMentionLength = maxUserNameLength + 5; // Because of the following format: <@&user> and the space const maxMessageWithMentionLength = maxMessageLength - maxMentionLength; +const maxEmbedLength = 6000; export type SendMessage = (options: Discord.MessageOptions) => Promise<any>; @@ -17,8 +18,7 @@ export abstract class DiscordUtils public static async sendMultiMessage ( sendMessage: SendMessage, messageTexts: string[], - components?: Component[], - imageUrl?: string + additions?: Additions ): Promise<void> { let entryCounter = messageTexts.length - 1; @@ -28,20 +28,35 @@ export abstract class DiscordUtils content: messageText, }; - if (entryCounter === 0) + if ((entryCounter === 0) && (additions !== undefined)) { - // Components and images must be attached to the last message we send. + // Optional additions must be attached to the last message we send. - if (components !== undefined) + if (typeof additions === 'string') { - const messageComponents = this.convertComponents(components); + const attachment = new Discord.MessageAttachment(additions); + messageOptions.attachments = [attachment]; + } + else if (this.isComponents(additions)) + { + const messageComponents = this.convertComponents(additions); messageOptions.components = messageComponents; } - - if (imageUrl !== undefined) + else if (this.isVisualisations(additions)) { - const attachment = new Discord.MessageAttachment(imageUrl); - messageOptions.attachments = [attachment]; + // The compact visualisations are added as a shared embed to the last message. + + const sharedCompactEmbed = new Discord.MessageEmbed(); + + for (const visualisation of additions) + { + if (visualisation.type == VisualisationType.Compact) + { + sharedCompactEmbed.addField(visualisation.headline, visualisation.text); + } + } + + messageOptions.embeds = [sharedCompactEmbed]; } } @@ -49,6 +64,42 @@ export abstract class DiscordUtils entryCounter--; } + + // All normal visualisations are send as seperate messages: + if ((additions !== undefined) && (this.isVisualisations(additions))) + { + let embeds: Discord.MessageEmbed[] = []; + let characterSum = 0; + + for (const visualisation of additions) + { + if (visualisation.type == VisualisationType.Normal) + { + const normalEmbed = new Discord.MessageEmbed(); + normalEmbed.setTitle(visualisation.headline); + normalEmbed.setDescription(visualisation.text); + + const characterCount = visualisation.headline.length + visualisation.text.length; + + if (characterSum + characterCount > maxEmbedLength) + { + await sendMessage({ embeds: embeds }); + embeds = [normalEmbed]; + characterSum = characterCount; + } + else + { + embeds.push(normalEmbed); + characterSum += characterCount; + } + } + } + + if (embeds.length > 0) + { + await sendMessage({ embeds: embeds }); + } + } } /** @@ -117,4 +168,36 @@ export abstract class DiscordUtils // TODO: What about multiple rows? return [actionRow]; } + + public static isComponents (additions: Additions): additions is Component[] + { + if (typeof additions === 'string') + { + return false; + } + else if (additions.length === 0) + { + return true; + } + else + { + return Object.values(ComponentType).includes(additions[0].type as ComponentType); + } + } + + public static isVisualisations (additions: Additions): additions is Visualisation[] + { + if (typeof additions === 'string') + { + return false; + } + else if (additions.length === 0) + { + return true; + } + else + { + return Object.values(VisualisationType).includes(additions[0].type as VisualisationType); + } + } } diff --git a/scripts/wichtelbot/message/handlingDefinition.ts b/scripts/wichtelbot/message/handlingDefinition.ts index adff56ad13c7dbde0d880ba2435d7a31363e6e9f..de5e8b1d9e330ad2ec91b3f2f1a9d7aa39effd54 100644 --- a/scripts/wichtelbot/message/handlingDefinition.ts +++ b/scripts/wichtelbot/message/handlingDefinition.ts @@ -50,6 +50,11 @@ export default class HandlingDefinition protected generalModule: GeneralModule; protected informationModule: InformationModule; + private get maxShortMessageLength (): number + { + return Math.floor(Config.main.maxMessageLength / 2); + } + constructor (generalModule: GeneralModule, informationModule: InformationModule) { this.generalModule = generalModule; @@ -200,6 +205,13 @@ export default class HandlingDefinition paths: null, handlerFunction: async (message: Message): Promise<void> => { + if (message.content.length > this.maxShortMessageLength) + { + await this.generalModule.sendMessageTooLong(message, this.maxShortMessageLength); + + return; + } + this.informationModule.setAddress(message); await this.generalModule.continue( message, @@ -261,6 +273,13 @@ export default class HandlingDefinition paths: null, handlerFunction: async (message: Message): Promise<void> => { + if (message.content.length > this.maxShortMessageLength) + { + await this.generalModule.sendMessageTooLong(message, this.maxShortMessageLength); + + return; + } + this.informationModule.setDigitalAddress(message); const neededInformationStates = this.informationModule.getListOfNeededInformationStates(message); diff --git a/scripts/wichtelbot/message/modules/generalModule.ts b/scripts/wichtelbot/message/modules/generalModule.ts index eb367a8ce1cf7363420259dbb1f0f190cf0f18e0..2201b3083d06f51776acb4dc6e71a74b69f0cbb7 100644 --- a/scripts/wichtelbot/message/modules/generalModule.ts +++ b/scripts/wichtelbot/message/modules/generalModule.ts @@ -1,5 +1,5 @@ import Localisation, { CommandInfo } from '../../../utility/localisation'; -import { Component } from '../../endpoint/definitions'; +import { Additions } from '../../endpoint/definitions'; import Config from '../../../utility/config'; import Contact from '../../classes/contact'; import ContactType from '../../types/contactType'; @@ -28,24 +28,24 @@ export default class GeneralModule * Will set User/Contact data for the TokenString. * @param text The text to reply. */ - public async reply (message: Message, text: TokenString, components?: Component[]): Promise<void> + public async reply (message: Message, text: TokenString, options?: Additions): Promise<void> { const whatIsThere = this.database.getWhatIsThere(message.author); const answer = text.process(whatIsThere); - await message.reply(answer, components); + await message.reply(answer, options); } /** * Sets the state of the contact, then replies. */ - public async continue (message: Message, state: State, text: TokenString, components?: Component[]): Promise<void> + public async continue (message: Message, state: State, text: TokenString, options?: Additions): Promise<void> { const contact = this.database.getContact(message.author.id); contact.state = state; this.database.updateContact(contact); - await this.reply(message, text, components); + await this.reply(message, text, options); } /** @@ -195,11 +195,11 @@ export default class GeneralModule } } - public async sendMessageTooLong (message: Message): Promise<void> + public async sendMessageTooLong (message: Message, maxLength?: number): Promise<void> { const parameters = new KeyValuePairList(); parameters.addPair('messageLength', `${message.content.length}`); - parameters.addPair('maxLength', `${Config.main.maxMessageLength}`); + parameters.addPair('maxLength', `${maxLength ?? Config.main.maxMessageLength}`); const answer = Localisation.texts.messageTooLong.process(message.author, parameters); diff --git a/scripts/wichtelbot/message/modules/informationModule.ts b/scripts/wichtelbot/message/modules/informationModule.ts index f801c03ca3c9e91bec692f708611af6ff05a0f06..f841c7130582d4b6975aa8b3fcd79fcd16578a84 100644 --- a/scripts/wichtelbot/message/modules/informationModule.ts +++ b/scripts/wichtelbot/message/modules/informationModule.ts @@ -1,3 +1,4 @@ +import { Visualisation, VisualisationType } from "../../endpoint/definitions"; import Config from "../../../utility/config"; import ContactType from "../../types/contactType"; import Database from "../../database";