-
Notifications
You must be signed in to change notification settings - Fork 65
Add ListMessageSplitter and apply to rules command #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,78 @@ | ||
| import {LogService} from "matrix-bot-sdk"; | ||
| /* | ||
| The list splitter is meant to help with large itemized messages (such as a ban list), | ||
| which each have "headers" (bits of text that introduce the list). | ||
|
|
||
| The behaviour is such that one pumps items and headers one-by-one into ListMessageSplitter, | ||
| which then splits them internally to accomodate maximum matrix event sizes, and renders them simultaniously. | ||
|
|
||
| As such, a workflow with ListMessageSplitter would look something like this; | ||
|
|
||
| ```js | ||
| let splitter = new ListMessageSplitter(); | ||
|
|
||
| // Start a new list, input both html and text versions of the header. | ||
| splitter.add_header("<b>Rules currently in use:</b>", "Rules currently in use:"); | ||
|
|
||
| for (rule of this.rules) { | ||
| // Add a new "paragraph", an item, se string templates here for each item. | ||
| splitter.add_paragraph( | ||
| `rule <code>#${rule.number}</code>: ${rule.text()}`, | ||
| `rule #${rule.number}: ${rule.text()}` | ||
| ) | ||
| } | ||
|
|
||
| if (this.rules.length === 0) { | ||
| splitter.add_paragraph( | ||
| "No rules configured", | ||
| "No rules configured" | ||
| ) | ||
| } | ||
|
|
||
| // Add another header, start a new list in the same message. | ||
| splitter.add_header("<b>Servers currently observed:</b>", "Servers currently observed:"); | ||
|
|
||
| for (server of this.servers) { | ||
| splitter.add_paragraph( | ||
| `server ${server.name()}`, | ||
| `server ${server.name()}` | ||
| ) | ||
| } | ||
|
|
||
| if (this.servers.length === 0) { | ||
| splitter.add_paragraph( | ||
| "No servers observed", | ||
| "No servers observed" | ||
| ) | ||
| } | ||
|
|
||
| // Reply to an event with the whole deal, splitting into multiple messages as needed. | ||
| splitter.reply(mjolnir.client, roomId, event, true) | ||
| ``` | ||
| */ | ||
|
|
||
| import {LogService, MatrixClient, RichReply} from "matrix-bot-sdk"; | ||
|
|
||
| // Helper type for html + text tuples. | ||
| export type MessageSnippet = { html: string, text: string }; | ||
|
|
||
| // The max size a message can be, with 24_000 picked at random to accommodate for overhead. | ||
| // | ||
| // Note: This amount was checked through trial and error, a conservative estimate would be | ||
| // 65_536 / 2, though this author does not know overhead estimates of matrix E2EE. | ||
| const OVERHEAD = 24_000 | ||
Gnuxie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const MAX_SIZE = 65_536 - OVERHEAD; | ||
|
|
||
| // The extra bits that a <ul> tag wrapping would add to a message | ||
| const UL_TAG_WRAP_SIZE = "<ul></ul>".length; | ||
| // The extra bits that a <il> list tag wrapping would add to a message | ||
| const HTML_LIST_ITEM_EXTRA_SIZE = "<li></li>".length; | ||
| // The extra bits that listification of plaintext items would add to a message. | ||
| const TEXT_LIST_ITEM_EXTRA_SIZE = " * \n".length; | ||
|
|
||
| // An "Item" object to push into the list splitter. | ||
| export class MessageListItem { | ||
| public html: string; | ||
| public text: string; | ||
| public readonly html: string; | ||
| public readonly text: string; | ||
|
|
||
| constructor( | ||
| html: string, | ||
|
|
@@ -34,35 +93,56 @@ export class MessageListItem { | |
| } | ||
| } | ||
|
|
||
| // A "header" object to push into the list splitter. | ||
| export class MessageListHeader extends MessageListItem { | ||
| public size(): number { | ||
| return this.html.length + this.text.length | ||
| } | ||
| } | ||
|
|
||
| // An internal helper class to hold a series of items, together with an optional header. | ||
| // | ||
| // Mainly provides a coherent split_at_size function that would allow for on-demand-sized splitting of | ||
| // listings with headers. | ||
| class MessageListing { | ||
| public items: MessageListItem[] = []; | ||
|
|
||
| constructor(public header: MessageListHeader | null) { | ||
| } | ||
|
|
||
| // Attempts to split this listing into a `sized` and `rest` listing. | ||
| // | ||
| // Returns: | ||
| // `sized` != null, if sized was adequate, or had to be split | ||
| // `rest` != null, if sized was not adequate (and had to be split), | ||
| // or the first item is too big to be split at the desired size. | ||
|
||
| public split_at_size(desired_size: number): {sized: MessageListing | null, rest: MessageListing | null} { | ||
ShadowJonathan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (this.size() <= desired_size) { | ||
| // If the current listing is under the desired size, just return it, rest = null. | ||
| return {sized: this, rest: null}; | ||
| } else { | ||
| // Else, split it. | ||
|
|
||
| // Create a new listing with just the current header, | ||
| // as a candidate for the new sized listing. | ||
| let sized = new MessageListing(this.header); | ||
| if (sized.size() > desired_size) { | ||
| // If the header alone is too much, just give up. | ||
| return {sized: null, rest: this} | ||
| } | ||
|
|
||
| // Create another listing, and dump the rest of all items in there. | ||
| let rest = new MessageListing(null); | ||
| // Be sure to shallow copy, to not disturb the `this` listing. | ||
| rest.items = [...this.items]; | ||
|
|
||
| // Perform initial shift of item into a variable. | ||
| let current_item = rest.items.shift(); | ||
|
|
||
| // Keep looping while there are still items left. | ||
| // (Replaced at the end of the loop) | ||
| while (current_item !== undefined) { | ||
| // Add the new item tentatively | ||
| // Add the new item to `sized` tentatively. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tentatively? Wouldn't it be safer/clearer to guard the push with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, because there might be things about a listing's size that are optimised or changed as result of an item being added. For example, if So it is safer to try, and then back off to the last version of the listing if it becomes too big.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohh, I see what you mean. That is quite unfortunate .
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also don't want to duplicate logic to calculate it, as that's prone to desync if the original logic is updated. |
||
| sized.items.push(current_item); | ||
|
|
||
| if (sized.size() > desired_size) { | ||
|
|
@@ -72,6 +152,7 @@ class MessageListing { | |
| if (sized.items.length > 0) { | ||
| return {sized, rest}; | ||
| } else { | ||
| // If this was the first item, `sized` is empty, return null. | ||
| return {sized: null, rest} | ||
| } | ||
| } | ||
|
|
@@ -96,6 +177,7 @@ class MessageListing { | |
| + this.items.reduce((prev, curr, _idx, _arr) => prev + curr.size(), 0); | ||
ShadowJonathan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Render this listing into a messagesnippet. | ||
| public render(): MessageSnippet { | ||
| let current: MessageSnippet = { | ||
| html: "", | ||
|
|
@@ -118,7 +200,7 @@ class MessageListing { | |
| } | ||
| } | ||
|
|
||
| /// A class that allows splitting items and headers into multiple messages. | ||
| // A class that allows splitting items and headers into multiple messages. | ||
| export class ListMessageSplitter { | ||
| private items: (MessageListItem | MessageListHeader)[] = []; | ||
|
|
||
|
|
@@ -191,14 +273,14 @@ export class ListMessageSplitter { | |
| return result; | ||
| } | ||
|
|
||
| public render(): { html: string, text: string }[] { | ||
| let rendered: { html: string, text: string }[] = []; | ||
| public render(): MessageSnippet[] { | ||
| let rendered: MessageSnippet[] = []; | ||
|
|
||
| let listings = this.get_listings(); | ||
| let chunks = this.split_listings(listings); | ||
|
|
||
| for (let chunk of chunks) { | ||
| let current = { | ||
| let current: MessageSnippet = { | ||
| html: "", | ||
| text: "", | ||
| } | ||
|
|
@@ -214,4 +296,24 @@ export class ListMessageSplitter { | |
|
|
||
| return rendered; | ||
| } | ||
|
|
||
| public async reply(client: MatrixClient, roomId: string, toEvent: any, m_notice: boolean) { | ||
| let rendered = this.render(); | ||
| let first = rendered.shift()!; | ||
|
|
||
| const reply = RichReply.createFor(roomId, toEvent, first.text, first.html); | ||
| if (m_notice) | ||
| reply["msgtype"] = "m.notice"; | ||
|
|
||
| await client.sendMessage(roomId, reply); | ||
|
|
||
| for (const message of rendered) { | ||
| await client.sendMessage(roomId, { | ||
| msgtype: m_notice ? "m.notice" : "m.text", | ||
| body: message.text, | ||
| format: "org.matrix.custom.html", | ||
| formatted_body: message.html, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.