From cc9c3552900c6696e5be63b79695f6c58b9328ab Mon Sep 17 00:00:00 2001 From: Benedikt Magnus <magnus@magnuscraft.de> Date: Sat, 6 Nov 2021 14:10:35 +0100 Subject: [PATCH] Added visualisation system besides the components. --- .../endpoint/definitions/additions.ts | 13 +++ .../endpoint/definitions/channel.ts | 9 +- .../wichtelbot/endpoint/definitions/index.ts | 3 + .../endpoint/definitions/message.ts | 8 +- .../wichtelbot/endpoint/definitions/user.ts | 9 +- .../visualisation/visualisation.ts | 8 ++ .../visualisation/visualisationType.ts | 5 + .../implementations/discord/discordChannel.ts | 6 +- .../discord/discordInteraction.ts | 8 +- .../implementations/discord/discordMessage.ts | 12 +- .../implementations/discord/discordUser.ts | 6 +- .../implementations/discord/discordUtils.ts | 105 ++++++++++++++++-- .../wichtelbot/message/handlingDefinition.ts | 19 ++++ .../message/modules/generalModule.ts | 14 +-- .../message/modules/informationModule.ts | 1 + 15 files changed, 172 insertions(+), 54 deletions(-) create mode 100644 scripts/wichtelbot/endpoint/definitions/additions.ts create mode 100644 scripts/wichtelbot/endpoint/definitions/visualisation/visualisation.ts create mode 100644 scripts/wichtelbot/endpoint/definitions/visualisation/visualisationType.ts diff --git a/scripts/wichtelbot/endpoint/definitions/additions.ts b/scripts/wichtelbot/endpoint/definitions/additions.ts new file mode 100644 index 0000000..99cbb12 --- /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 1ce07a3..632597d 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 d33af63..65bbda3 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 a837e77..1d75aff 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 d530469..8459a84 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 0000000..436dfad --- /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 0000000..05d0ac2 --- /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 c58de94..0a245fd 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 5950169..25b4360 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 2a2c2d4..9025cd5 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 d6d19e8..74e4cd7 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 ab8e482..7f7a003 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 adff56a..de5e8b1 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 eb367a8..2201b30 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 f801c03..f841c71 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"; -- GitLab