Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions src/modules/chimeIn.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import { info, warn, error as logError } from '../logger.js';
import { OPENCLAW_URL, OPENCLAW_TOKEN } from './ai.js';
import { splitMessage, needsSplitting } from '../utils/splitMessage.js';

// ── Per-channel state ──────────────────────────────────────────────────────────
// Map<channelId, { messages: Array<{author, content}>, counter: number, lastActive: number, abortController: AbortController|null }>
Expand Down Expand Up @@ -258,20 +259,8 @@ export async function accumulate(message, config) {
warn('ChimeIn suppressed empty response', { channelId });
} else {
// Send as a plain channel message (not a reply)
if (response.length > 2000) {
// Split on word boundaries to avoid breaking mid-word/URL/emoji
const chunks = [];
let remaining = response;
while (remaining.length > 0) {
if (remaining.length <= 1990) {
chunks.push(remaining);
break;
}
let splitAt = remaining.lastIndexOf(' ', 1990);
if (splitAt <= 0) splitAt = 1990;
chunks.push(remaining.slice(0, splitAt));
remaining = remaining.slice(splitAt).trimStart();
}
if (needsSplitting(response)) {
const chunks = splitMessage(response);
for (const chunk of chunks) {
await message.channel.send(chunk);
}
Expand Down
5 changes: 3 additions & 2 deletions src/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sendWelcomeMessage } from './welcome.js';
import { isSpam, sendSpamAlert } from './spam.js';
import { generateResponse } from './ai.js';
import { accumulate, resetCounter } from './chimeIn.js';
import { splitMessage, needsSplitting } from '../utils/splitMessage.js';

/**
* Register bot ready event handler
Expand Down Expand Up @@ -100,8 +101,8 @@ export function registerMessageCreateHandler(client, config, healthMonitor) {
);

// Split long responses
if (response.length > 2000) {
const chunks = response.match(/[\s\S]{1,1990}/g) || [];
if (needsSplitting(response)) {
const chunks = splitMessage(response);
for (const chunk of chunks) {
await message.channel.send(chunk);
}
Expand Down
61 changes: 61 additions & 0 deletions src/utils/splitMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Split Message Utility
* Splits long messages to fit within Discord's 2000-character limit.
*/

/**
* Discord's maximum message length.
*/
const DISCORD_MAX_LENGTH = 2000;

/**
* Safe chunk size leaving room for potential overhead.
*/
const SAFE_CHUNK_SIZE = 1990;

/**
* Splits a message into chunks that fit within Discord's character limit.
* Attempts to split on word boundaries to avoid breaking words, URLs, or emoji.
*
* @param {string} text - The text to split
* @param {number} [maxLength=1990] - Maximum length per chunk (default 1990 to stay under 2000)
* @returns {string[]} Array of text chunks, each within the specified limit
*/
export function splitMessage(text, maxLength = SAFE_CHUNK_SIZE) {
if (!text || text.length <= maxLength) {
return text ? [text] : [];
}

const chunks = [];
let remaining = text;

while (remaining.length > 0) {
if (remaining.length <= maxLength) {
chunks.push(remaining);
break;
}

// Try to find a space to split on (word boundary)
let splitAt = remaining.lastIndexOf(' ', maxLength);

// If no space found or it's at the start, force split at maxLength
if (splitAt <= 0) {
splitAt = maxLength;
}

chunks.push(remaining.slice(0, splitAt));
remaining = remaining.slice(splitAt).trimStart();
}

return chunks;
}

/**
* Checks if a message exceeds Discord's character limit.
*
* @param {string} text - The text to check
* @returns {boolean} True if the message needs splitting
*/
export function needsSplitting(text) {
return text && text.length > DISCORD_MAX_LENGTH;
}