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