Bot Message Handler Implementation

Created on: September 28, 2024

Answered using OpenAI 01 Preview by OpenAI01

Question

<file path="./package.json">
{
"name": "your-project",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"html-entities": "^2.5.2",
"node-telegram-bot-api": "^0.66.0",
"redis": "^4.7.0",
"validator": "^13.12.0"
}
}
</file>
<file path="./config/config.js">
export const port = process.env.PORT || 8000
export const botToken = process.env.BOT_TOKEN
export const botUsername = process.env.BOT_USERNAME
export const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'

if (!botToken || !botUsername) {
throw new Error('BOT_TOKEN and BOT_USERNAME must be set in environment variables.')
}
</file>
<file path="./middleware/validateSessionId.js">
export const validateSessionId = (req, res, next) => {
const sessionId = (req.params.sessionId || '').trim()

if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
return res.status(400).json({ error: 'Invalid Session ID.' })
}

req.sessionId = sessionId
next()
}
</file>
<file path="./index.js">
import express from 'express'
import commandsRoutes from './routes/commands'
import updateRoutes from './routes/update'
import sessionsRoutes from './routes/sessions'
import { redisClient } from './utils/redisClient'
import { port } from './config/config'

const app = express()

app.use(express.json())
app.use('/commands', commandsRoutes)
app.use('/update', updateRoutes)
app.use('/sessions', sessionsRoutes)

const server = app.listen(port, () => {
console.log(`Server is listening on port ${port}`)
})

process.on('SIGINT', async () => {
console.log('Shutting down server...')
try {
await redisClient.quit()
server.close(() => {
console.log('Server closed.')
process.exit(0)
})
} catch (err) {
console.error('Error during shutdown:', err)
process.exit(1)
}
})
</file>
<file path="./bot/handlers.js">
import { bot } from './instance'
import { parseSessionId, withUserSession, startSession } from '../utils/session'

const start = async (chatId, userId, text) => {
const sessionId = parseSessionId(text)
if (!sessionId) return bot.sendMessage(chatId, 'Please provide a session ID.')

await startSession(sessionId, userId, chatId)
await bot.sendMessage(chatId, `Welcome to session ${sessionId}!`)
}

const clear = (chatId, userId) => {
return withUserSession(chatId, userId, (session) => {
session.commands.push({ type: 'CLEAR' })
session.promptNumber = 2
return bot.sendMessage(chatId, 'Session cleared.')
})
}

const request = (chatId, userId, text) => {
return withUserSession(chatId, userId, (session) => {
session.commands.push({ type: 'RUN', text, promptNumber: session.promptNumber })
session.promptNumber += 2
return bot.sendMessage(chatId, 'Processing your request...')
})
}

export const handleMessage = async (msg) => {
const chatId = msg.chat.id
const userId = msg.from.id
const text = msg.text?.trim() || ''

try {
if (text.startsWith('/start')) await start(chatId, userId, text)
else if (text.startsWith('/clear')) await clear(chatId, userId)
else await request(chatId, userId, text)
} catch (error) {
console.error('Error handling message:', error)
bot.sendMessage(chatId, 'An error occurred.')
}
}
</file>
<file path="./bot/instance.js">
import TelegramBot from 'node-telegram-bot-api'
import { botToken } from '../config/config'
import { handleMessage } from './handlers'

export const bot = new TelegramBot(botToken, { polling: true })

bot.on('message', handleMessage)
</file>
<file path="./controllers/bot.js">
import { withExistingSession } from '../utils/session'
import { bot } from '../bot/instance'
import { splitMessage } from '../utils/splitMessage'

export const processSession = async (sessionId) => {
await withExistingSession(sessionId, async (session) => {
session.processing = true
try {
while (session.results.length > 0) {
const resultText = session.results.shift()
await processMessages(session, resultText)
}
} catch (error) {
console.error(`Error processing session ${sessionId}:`, error)
} finally {
session.processing = false
}
})
}

export const processMessages = async (session, text) => {
if (!session.chatId) return
if (!session.messages) session.messages = []

try {
for (const chunk of splitMessage(text)) {
const message = await bot.sendMessage(session.chatId, chunk, { parse_mode: 'MarkdownV2' })
session.messages.push(message)
}
} catch (error) {
console.error(`Error sending messages for session ${session.sessionId}:`, error)
}
}
</file>
<file path="./routes/sessions.js">
import express from 'express'
import { redisClient } from '../utils/redisClient'

const router = express.Router()

router.get('/', async (req, res) => {
try {
const keys = await redisClient.keys('session:*')
const sessionIds = keys.map(key => key.replace('session:', ''))
res.json({ sessions: sessionIds })
} catch (error) {
console.error('Error fetching sessions:', error)
res.status(500).json({ error: 'Internal Server Error' })
}
})

export default router
</file>
<file path="./routes/commands.js">
import express from 'express'
import { validateSessionId } from '../middleware/validateSessionId'
import { withExistingOrNewSession } from '../utils/session'

const router = express.Router()

router.get('/:sessionId', validateSessionId, async (req, res) => {
const { sessionId } = req

try {
await withExistingOrNewSession(sessionId, (session) => {
const commands = [...session.commands]
session.commands = []
res.json({ commands })
})
} catch (error) {
console.error(`Error fetching commands for session ${sessionId}:`, error)
res.status(500).json({ error: 'Internal Server Error' })
}
})

router.post('/:sessionId', validateSessionId, async (req, res) => {
const { sessionId } = req
const command = req.body

try {
await withExistingOrNewSession(sessionId, (session) => {
if (!command || !command.type) {
return res.status(400).json({ error: 'Invalid command.' })
}

if (command.type === 'CLEAR') {
session.promptNumber = 2
} else {
command.promptNumber = session.promptNumber
session.promptNumber += 2
}

session.commands.push(command)
res.json({ message: 'Command added.' })
})
} catch (error) {
console.error(`Error adding command to session ${sessionId}:`, error)
res.status(500).json({ error: 'Internal Server Error' })
}
})

export default router
</file>
<file path="./routes/password.js">
import express from 'express'
import validator from 'validator'
import bcrypt from 'bcrypt'
import { validateSessionId } from '../middleware/validateSessionId'
import { withExistingOrNewSession } from '../utils/session'
import { botUsername } from '../config/config'

const router = express.Router()

router.post('/:sessionId', validateSessionId, async (req, res) => {
const { sessionId } = req
const { password } = req.body

try {
if (!password || validator.isEmpty(password)) {
return res.status(400).json({ error: 'Password is required.' })
}

const sanitizedPassword = validator.escape(password)

await withExistingOrNewSession(sessionId, async (session) => {
session.password = await bcrypt.hash(sanitizedPassword, 10)
})

res.json({ link: `https://t.me/${botUsername}?start=${sessionId}-${sanitizedPassword}` })
} catch (error) {
console.error(`Error setting password for session ${sessionId}:`, error)
res.status(500).json({ error: 'Internal Server Error' })
}
})

export default router
</file>
<file path="./routes/update.js">
import express from 'express'
import { validateSessionId } from '../middleware/validateSessionId'
import { getOrCreateSession, setSession } from '../utils/session'
import { bot } from '../bot/instance'

const router = express.Router()

router.post('/:sessionId', validateSessionId, async (req, res) => {
const { sessionId } = req
const resultText = req.body.text

try {
if (!resultText) {
return res.status(400).json({ error: 'Result text is required.' })
}

let session = await getOrCreateSession(sessionId)

session.results.push(resultText)
await setSession(sessionId, session)

if (session.chatId) {
await bot.sendMessage(session.chatId, resultText)
}

res.json({ message: 'Result received.' })
} catch (error) {
console.error(`Error updating session ${sessionId}:`, error)
res.status(500).json({ error: 'Internal Server Error' })
}
})

export default router
</file>
<file path="./utils/redisClient.js">
import { createClient } from 'redis'
import { redisUrl } from '../config/config'

export const redisClient = createClient({ url: redisUrl })

redisClient.on('error', (err) => console.error('Redis error:', err))

redisClient.connect().then(() => {
console.log('Connected to Redis.')
}).catch((error) => {
console.error('Failed to connect to Redis:', error)
process.exit(1)
})
</file>
<file path="./utils/markdownV2Text.js">
import { decode } from "html-entities";

function parseFormatting(node, type, symbol_left, symbol_right = symbol_left) {
if (!node.formatting) node.formatting = {}
if (!node.formatting[type]) node.formatting[type] = []

const escaped_left = symbol_left.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&')
const escaped_right = symbol_right.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&')

for (const match of node.value.matchAll(new RegExp(`${escaped_left}\\S.*?\\S${escaped_right}`, 'g'))) {
const left = match.index
const right = match.index + match[0].length - symbol_right.length
fixFormattingRanges(node.formatting, -1, left, symbol_left.length)
fixFormattingRanges(node.formatting, -1, right, symbol_right.length)
}

let removedLength = 0

const handle = (match, captured, index) => {
const begin = index - removedLength
const end = begin + captured.length
node.formatting[type].push({ begin, end })

removedLength += match.length - captured.length
return captured
}

node.value = node.value.replace(new RegExp(`${escaped_left}(\\S.*?\\S)${escaped_right}`, 'g'), handle)

removedLength = 0

node.value = node.value.replace(new RegExp(`${escaped_left}(\\S.*)$`), handle)
}
const formatting = [
(node) => node.value = decode(node.value),
(node) => parseFormatting(node, 'monospace', '`'),
(node) => parseFormatting(node, 'bold', '**'),
(node) => parseFormatting(node, 'italic', '*'),
(node) => parseFormatting(node, 'italic', '_'),
(node) => parseFormatting(node, 'strikethrough', '~~'),
(node) => parseFormatting(node, 'strikethrough', '~'),
(node) => parseFormatting(node, 'underline', '<u>', '</u>'),
(node) => parseFormatting(node, 'spoiler', '<spoiler>', '</spoiler>')
]

function iterateFormatting(formatting, callback) {
for (const type in formatting) {
formatting[type].forEach(f => {
callback(f, type)
})
}
}

function fixFormattingRanges(formatting, diff, index, count = 1) {
for (let i = index; i < index + count; i++) {
iterateFormatting(formatting, (f) => {
if (index < f.begin) f.begin += diff
if (index <= f.end) f.end += diff
})
}
}

function fixFormattingRangesV2(formatting, diff, index, count = 1) {
for (let i = index; i < index + count; i++) {
iterateFormatting(formatting, (f) => {
if (index < f.begin) f.begin += diff
if (index < f.end) f.end += diff
})
}
}

function symbolOfFormattingType(type) {
switch (type) {
case 'bold': return '*'
case 'italic': return '_'
case 'monospace': return '`'
case 'strikethrough': return '~'
case 'underline': return '__'
case 'spoiler': return '||'
}
}

function escapeMarkdownV2(node) {
let counter = 0
node.value = node.value.replace(/[\\\[\]\-(){}+_*~`>#=|!.]/g, (match, index) => {
fixFormattingRangesV2(node.formatting, +1, index + counter)
counter++
return '\\' + match
})
}

function parseText(node) {
for (const parse of formatting) {
parse(node)
}
return node
}

export function markdownV2Text(input_node) {
const node = { ...input_node }
parseText(node)
escapeMarkdownV2(node)
let text = node.value

function insert(i, symbol) {
fixFormattingRanges(node.formatting, +symbol.length, i)
text = text.substring(0, i) + symbol + text.substring(i)
}

iterateFormatting(node.formatting, (f, type) => {
insert(f.begin, symbolOfFormattingType(type))
insert(f.end, symbolOfFormattingType(type))
})

text = text.replace(/\\!\\\[(?:\S|\S.*?\S)\\]\\\((?:\S|\S.*?\S)\\\)/g, (match) => {
const match1 = match.matchAll(/\\\[(\S|\S.*?\S)\\]\\\((\S|\S.*?\S)\\\)/g).next().value
return `![${match1[1]}](${match1[2]})`
})

text = text.replace(/\\\[(?:\S|\S.*?\S)\\]\\\((?:\S|\S.*?\S)\\\)/g, (match) => {
const match1 = match.matchAll(/\\\[(\S|\S.*?\S)\\]\\\((\S|\S.*?\S)\\\)/g).next().value
return `[${match1[1]}](${match1[2]})`
})

return text
}
</file>
<file path="./utils/markdownV2.js">
import { markdownV2Text } from "./markdownV2Text"

function parser(node, regex, type, parse = (text) => {return text}) {
if (node.type !== 'text') {
return [node]
}

const text = node.value
const matches = text.matchAll(regex)
const nodes = []
let lastIndex = 0

for (const match of matches) {
const value = parse(match[1])
const startIndex = match.index

if (startIndex > lastIndex) {
nodes.push({ type: 'text', value: text.substring(lastIndex, startIndex) })
}

nodes.push({ type, value })

lastIndex = startIndex + match[0].length
}

if (lastIndex < text.length) {
nodes.push({ type: 'text', value: text.substring(lastIndex) })
}

return nodes
}

const parsers = [
(node) => parser(node, /^```(.*?\n)```$/gms, 'code'),
(node) => parser(node, /^`(.*?\n)`$/gms, 'code'),
(node) => parser(node, /(((^\|.*)+\n?)+)/gm, 'table', (text) => {
const result = text.trim().split('\n').map(row => {
const cells = row.split('|').slice(1)
if (cells[cells.length - 1].trim() === '') {
cells.pop()
}
return cells.map(cell => cell.trim())
})
result.splice(1, 1)
return result
}),
(node) => parser(node, /^>(.*)/gm, 'quote', (text) => [{
type: 'text',
value: text,
}]),
(node) => parser(node, /^(#{1,6} .*)/gm, 'header', (text) => [{
type: 'text',
value: text.trim()
.replace(/\*\*(\S.*?\S)\*\*/g, (match, captured) => captured)
.replace(/\*\*(\S.*)$/g, (match, captured) => captured),
formatting: {
bold: [{ begin: 0, end: text.length }]
},
}]),
]

function escape(text) {
return text.replace(/[\\\[\]\-(){}+_*~`>#=|!.]/g, (match) => '\\' + match)
}

export function markdownV2(nodes) {
let output = ''
for (const node of nodes) {
switch (node.type) {
case 'text':
output += markdownV2Text(node) + '\n'
break
case 'code':
const codeBlock = '```' + escape(node.value) + '```'
let remainingCode = codeBlock

while (remainingCode.length > 0) {
const remainingSpace = MAX_OUTPUT_SIZE - output.length
const chunkLength = Math.min(remainingCode.length, remainingSpace)

output += remainingCode.substring(0, chunkLength)
remainingCode = remainingCode.substring(chunkLength)

if (remainingCode.length > 0) {
overflow()
}
}
break
case 'table':
output += '```markdown\n'

const rows = node.value
const maxLengths = rows[0].map((header, i) =>
Math.max(header.length, ...rows.slice(1).map(row => row[i] ? row[i].length : 0))
)

for (let i = 0; i < rows.length; i++) {
const row = rows[i]
output += escape('| ' + row.map((cell, j) => cell.padEnd(maxLengths[j])).join(' | ') + ' |\n')
if (i === 0) {
output += escape('| ' + maxLengths.map(length => '-'.repeat(length)).join(' | ') + ' |\n')
}
}

output += '```\n'
break
case 'quote':
output += '>' + markdownV2(node.value)
break
case 'header':
output += markdownV2(node.value)
break
}
}
if (output[output.length - 1] === '\n') {
output = output.slice(0, -1)
}
return output
}

export function parse(text) {
let in_nodes = [{type: 'text', value: text}]
for (const fn of parsers) {
const out_nodes = []
for (let i = 0; i < in_nodes.length; i++) {
out_nodes.push(...fn(in_nodes[i]))
}
in_nodes = out_nodes
}
return in_nodes
}
</file>
<file path="./utils/splitMessage.js">
import { parse, markdownV2 } from './markdownV2'

export const splitMessage = (text) => {
const maxLength = 4096
const formattedText = markdownV2(parse(text))
const messages = []

let remainingText = formattedText

while (remainingText.length > 0) {
if (remainingText.length <= maxLength) {
messages.push(remainingText)
break
}
let splitIndex = remainingText.lastIndexOf('\n', maxLength)
if (splitIndex === -1) {
splitIndex = maxLength
}
const messagePart = remainingText.slice(0, splitIndex).trim()
messages.push(messagePart)
remainingText = remainingText.slice(splitIndex).trim()
}
return messages
}
</file>
<file path="./utils/session.js">
import { redisClient } from './redisClient'
import { bot } from '../bot/instance'

const SESSION_TTL = 60 * 60 * 24

export const getSessionKey = (sessionId) => `session:${sessionId}`
export const getUserSessionKey = (userId) => `userSession:${userId}`

export const getSession = async (sessionId) => {
const data = await redisClient.get(getSessionKey(sessionId))
return data ? JSON.parse(data) : null
}

export const setSession = async (sessionId, session) => {
await redisClient.setEx(getSessionKey(sessionId), SESSION_TTL, JSON.stringify(session))
}

export const getUserSession = async (userId) => {
return await redisClient.get(getUserSessionKey(userId))
}

export const setUserSession = (userId, sessionId) => {
return redisClient.setEx(getUserSessionKey(userId), SESSION_TTL, sessionId)
}

export const createSession = async (sessionId) => {
const session = {
commands: [],
results: [],
promptNumber: 2,
userId: null,
chatId: null,
processing: false,
}
await setSession(sessionId, session)
return session
}

export const parseSessionId = (text) => {
const parts = text.trim().split(' ')
return parts.length > 1 ? parts[1] : null
}

export const getOrCreateSession = async (sessionId) => {
return (await getSession(sessionId)) || (await createSession(sessionId))
}

export const withUserSession = async (chatId, userId, update) => {
const sessionId = await getUserSession(userId)
if (!sessionId) {
await bot.sendMessage(chatId, 'No active session. Use /start <session_id> to start.')
return null
}

let session = await getSession(sessionId)
if (!session) {
await bot.sendMessage(chatId, 'Session not found.')
return null
}

const result = await update(session)
await setSession(sessionId, session)
return result
}

export const withExistingOrNewSession = async (sessionId, update) => {
let session = await getOrCreateSession(sessionId)
await update(session)
await setSession(sessionId, session)
}

export const withExistingSession = async (sessionId, update) => {
let session = await getSession(sessionId)
if (!session) return false

await update(session)
await setSession(sessionId, session)
return true
}

export const startSession = async (sessionId, userId, chatId) => {
const session = await getOrCreateSession(sessionId)
session.userId = userId
session.chatId = chatId
await setSession(sessionId, session)
await setUserSession(userId, sessionId)
}
</file>
<file path="./FORMATTING.md">
Forget all ";"
Forget all ".js" extensions in imports
Forget 2-4 spaces, use tabs
</file>
<file path="./INSTRUCTIONS.md">
Your task is to rewrite the code of the file utils/markdownV2.js. This is a **complex task** that requires careful consideration of various edge cases.

You should modify the markdownV2 function so that it returns a list of messages, each with a maximum length of 4096 characters. **While doing so, please adhere to best programming practices to ensure the resulting code is efficient, readable, and maintainable.**

**Here's how you should handle the splitting:**

* **Prioritize splitting by lines.** It's preferable to keep lines intact whenever possible.
* **Respect the 4096 character limit.** If adding more content to the current message would exceed the limit, create a new message and continue there. The previous message should remain as it is.
* **Apply the same logic to tables and code blocks.** Treat them as separate entities when splitting.
* **Handle cases where a table, code block, or even a single line of text exceeds 4096 characters.**
* If an element is too large to fit in a single message, truncate it to 4096 characters.
* For lines, try to split by words. If a single word is longer than 4096 characters, truncate it and continue the rest in subsequent messages.
* **Splitting tables:** Divide tables into parts by rows. If a row exceeds 4096 characters, you can split it by words. If a word within a row is longer than 4096 characters, truncate it as described for text.
* **Splitting code blocks:** Apply the same splitting logic as for tables.


Essentially, you need to ensure that no single message returned by the markdownV2 function exceeds 4096 characters while preserving the structure of the Markdown content as much as possible.
</file>

Thought

Clarifying requirements

The task involves rewriting utils/markdownV2.js to ensure the markdownV2 function returns a list of messages, each up to 4096 characters, prioritizing line splitting for intact lines.

Applying logic to splitting

I’m noting that tables, code blocks, and text should be treated separately when splitting. If any exceed 4096 characters, truncate or split by words or rows as needed.

Ensuring efficient formatting

I’m pulling together the instructions to ensure that the markdownV2 function in ./utils/markdownV2.js has a maximum of 4096 characters per message. The focus is on using tabs over spaces, omitting semicolons, and removing .js extensions in imports.

Mapping out parsing

I’m navigating the markdownV2.js file, examining parsing functions for elements like code blocks, tables, and headers, and preparing to modify markdownV2 for max 4096 character messages.

Rewriting markdownV2

I’m analyzing markdownV2’s structure, aiming to convert it into a list of 4096-character maximized messages. This involves detailed parsing and strategic splitting based on certain rules.

Mapping progress

I’m outlining a function to manage message tracking and splitting. Ensuring code stays under 4096 characters, intriguing details about structured splitting.

Crafting a message manager

I’m piecing together a helper class or function to keep track of messages, ensuring no line exceeds 80 or 100 characters. This involves managing the messages array, the current message, and its length.

Analyzing markdownV2

I’m examining the function markdownV2, which formats different node types into markdown format, managing code blocks, tables, and other markdown elements, and controlling output size with MAX_OUTPUT_SIZE.

Crafting a message manager

I'm piecing together a MessageBuilder class, with a maxLength setup, storing messages, and managing text additions and splits.

Setting up constants

I’m defining the maximum message length and creating the MessageBuilder class. It has a list to store messages and a currentMessage field for ongoing text, with methods to add and split text as needed.

Enhancing text addition

I'm thinking about refining the addText function by splitting text intelligently when it exceeds maxLength. This involves locating the last newline character, then trying to split at the last space if no newline is present.

Breaking down the solution

I’m working on a method to add text, ensuring it fits within a defined boundary. Text is split by newline or space if needed, and excess leading whitespace is removed.

Crafting methods

I'm working through the MessageBuilder class, planning to implement different methods for adding text, code, and table nodes to handle various content types effectively.

Crafting messages

I’m piecing together a function to format messages using MarkdownV2. It smoothly handles text, code blocks, and tables, ensuring they stay intact without splitting.

Adjusting for limits

I’m noting the requirement to split code blocks or tables if they exceed message length constraints. This ensures the content can be reviewed and utilized effectively, even in parts.

Ensuring manageable text length

I'm noting the strategy of splitting texts into manageable lines or chunks, maintaining a consistent character limit per line, and starting new messages as needed. This approach efficiently handles longer texts.

Taking a closer look

I’m addressing the need to split code blocks and tables carefully to ensure accuracy and smooth functionality.

Constructing code blocks

I'm thinking through how to implement the addCodeBlock function by appending code within length constraints, and addLargeBlock for splitting and adding oversized code blocks.

Refining the plan

I’m working through refining the logic by creating methods to manage code blocks and tables. This step-by-step approach ensures precision and clarity.

Breaking down message length

I’m working on the MessageBuilder class, splitting text and code blocks to fit within a maximum length, ensuring each message and code block is appropriately partitioned.

Integrating table handling

It's becoming clear that I need to define addTable(node) and implement processTable(node) to generate and pass table text to addCodeBlock within the markdownV2 function.

Constructing a Markdown table

I’m working through creating a Markdown table in the 'table' case. By processing each row, calculating maximum lengths, and formatting cells, I ensure the table is aligned properly.

Mapping out the solution

I’m piecing together how to manage exceptions in code blocks, especially for lines exceeding the maximum length. This involves tweaking the addLargeBlock method for splitting long lines effectively.

Updating methods

I’m tweaking functions to efficiently manage text in large blocks, ensuring they fit within length limits and handling long lines effectively.

Evaluating code structure

I'm breaking down methods, suggesting modularization for efficiency, readability, and maintainability, ensuring all edge cases are well-managed. Progressing towards code completion.

Designing the message builder

I’m crafting a MessageBuilder class to manage text and code block messages, ensuring they respect a specified maximum length. The class is structured to handle multiline inputs, split overly long lines, and flush completed messages to a list.

Processing various node types

Inside the markdownV2 function, nodes are processed based on their type, such as 'text', 'code', 'table', 'quote', and 'header', leading to the construction of markdown representations.

Adapting code formatting

OK, let's streamline the code by removing semicolons, replacing spaces with tabs, and eliminating .js from imports. This results in a cleaner, more standardized style.

Refining the method

I’m working on optimizing message handling by adding addCodeBlock, addText, and addLargeBlock methods. They manage text and code content efficiently, ensuring messages stay within the max length limit.

Answer

import { markdownV2Text } from './markdownV2Text' const MAX_MESSAGE_LENGTH = 4096 class MessageBuilder { constructor(maxLength = MAX_MESSAGE_LENGTH) { this.maxLength = maxLength this.messages = [] this.currentMessage = '' } addText(text) { const lines = text.split('\n') for (const line of lines) { const lineWithNewline = line + '\n' if (this.currentMessage.length + lineWithNewline.length <= this.maxLength) { this.currentMessage += lineWithNewline } else { if (lineWithNewline.length > this.maxLength) { this.splitLongLine(lineWithNewline) } else { this.flushCurrentMessage() this.currentMessage += lineWithNewline } } } } splitLongLine(line) { let remainingLine = line while (remainingLine.length > 0) { let remainingSpace = this.maxLength - this.currentMessage.length if (remainingSpace <= 0) { this.flushCurrentMessage() remainingSpace = this.maxLength } if (remainingLine.length <= remainingSpace) { this.currentMessage += remainingLine remainingLine = '' } else { let splitIndex = remainingLine.lastIndexOf(' ', remainingSpace - 1) if (splitIndex === -1 || splitIndex === 0) { splitIndex = remainingSpace } const chunk = remainingLine.slice(0, splitIndex) this.currentMessage += chunk remainingLine = remainingLine.slice(splitIndex).replace(/^\s+/, '') this.flushCurrentMessage() } } } addCodeBlock(codeBlock) { const codeLines = codeBlock.split('\n') const blockStart = codeLines[0] + '\n' const blockEnd = '\n' + codeLines[codeLines.length - 1] const contentLines = codeLines.slice(1, -1) this.addLargeBlock(contentLines, blockStart, blockEnd) } addLargeBlock(lines, blockStart, blockEnd) { let currentBlock = blockStart for (const line of lines) { const lineWithNewline = line + '\n' if (currentBlock.length + lineWithNewline.length + blockEnd.length <= this.maxLength) { currentBlock += lineWithNewline } else { if (lineWithNewline.length + blockStart.length + blockEnd.length <= this.maxLength) { currentBlock += blockEnd this.addText(currentBlock) this.flushCurrentMessage() currentBlock = blockStart + lineWithNewline } else { const splitLines = this.splitLongCodeLine(lineWithNewline, this.maxLength - blockStart.length - blockEnd.length) for (const splitLine of splitLines) { if (currentBlock.length + splitLine.length + blockEnd.length > this.maxLength) { currentBlock += blockEnd this.addText(currentBlock) this.flushCurrentMessage() currentBlock = blockStart } currentBlock += splitLine } } } } currentBlock += blockEnd this.addText(currentBlock) } splitLongCodeLine(line, maxLength) { const splitLines = [] let remainingLine = line while (remainingLine.length > 0) { if (remainingLine.length <= maxLength) { splitLines.push(remainingLine) remainingLine = '' } else { const chunk = remainingLine.slice(0, maxLength) splitLines.push(chunk) remainingLine = remainingLine.slice(maxLength) } } return splitLines } flushCurrentMessage() { if (this.currentMessage.length > 0) { this.messages.push(this.currentMessage.trim()) this.currentMessage = '' } } } function escape(text) { return text.replace(/[\\$$$$\-(){}+_*~`>#=|!.]/g, (match) => '\\' + match) } function processTable(node) { let tableText = '```markdown\n' const rows = node.value const maxLengths = rows[0].map((header, i) => Math.max(header.length, ...rows.slice(1).map(row => row[i] ? row[i].length : 0)) ) for (let i = 0; i < rows.length; i++) { const row = rows[i] tableText += escape('| ' + row.map((cell, j) => cell.padEnd(maxLengths[j])).join(' | ') + ' |\n') if (i === 0) { tableText += escape('| ' + maxLengths.map(length => '-'.repeat(length)).join(' | ') + ' |\n') } } tableText += '```\n' return tableText } function markdownV2(nodes) { const builder = new MessageBuilder(MAX_MESSAGE_LENGTH) for (const node of nodes) { switch (node.type) { case 'text': const text = markdownV2Text(node) + '\n' builder.addText(text) break case 'code': const codeBlock = '```' + escape(node.value) + '```\n' builder.addCodeBlock(codeBlock) break case 'table': const tableText = processTable(node) builder.addCodeBlock(tableText) break case 'quote': const quoteText = '>' + markdownV2([node.value]) + '\n' builder.addText(quoteText) break case 'header': const headerText = markdownV2([node.value]) + '\n' builder.addText(headerText) break default: break } } builder.flushCurrentMessage() return builder.messages } function parser(node, regex, type, parse = (text) => text) { if (node.type !== 'text') { return [node] } const text = node.value const matches = text.matchAll(regex) const nodes = [] let lastIndex = 0 for (const match of matches) { const value = parse(match[1]) const startIndex = match.index if (startIndex > lastIndex) { nodes.push({ type: 'text', value: text.substring(lastIndex, startIndex) }) } nodes.push({ type, value }) lastIndex = startIndex + match[0].length } if (lastIndex < text.length) { nodes.push({ type: 'text', value: text.substring(lastIndex) }) } return nodes } const parsers = [ (node) => parser(node, /^```(.*?\n)```$/gms, 'code'), (node) => parser(node, /^`(.*?\n)`$/gms, 'code'), (node) => parser(node, /(((^\|.*)+\n?)+)/gm, 'table', (text) => { const result = text.trim().split('\n').map(row => { const cells = row.split('|').slice(1) if (cells[cells.length - 1].trim() === '') { cells.pop() } return cells.map(cell => cell.trim()) }) result.splice(1, 1) return result }), (node) => parser(node, /^>(.*)/gm, 'quote', (text) => ({ type: 'text', value: text })), (node) => parser(node, /^(#{1,6} .*)/gm, 'header', (text) => ({ type: 'text', value: text.trim().replace(/\*\*(\S.*?\S)\*\*/g, '$1').replace(/\*\*(\S.*)$/g, '$1'), formatting: { bold: [{ begin: 0, end: text.length }] } })) ] export function parse(text) { let in_nodes = [{ type: 'text', value: text }] for (const fn of parsers) { const out_nodes = [] for (const node of in_nodes) { out_nodes.push(...fn(node)) } in_nodes = out_nodes } return in_nodes } export function markdownV2Wrapper(text) { const nodes = parse(text) return markdownV2(nodes) }

Explanation:

  • MessageBuilder Class: Manages message construction, ensuring no message exceeds 4096 characters.
    • addText: Splits text by lines and adds them, prioritizing line integrity.
    • splitLongLine: Splits lines longer than the limit by words, truncating if necessary.
    • addCodeBlock & addLargeBlock: Handles code blocks and tables, splitting by lines and ensuring proper formatting.
  • Parsing Functions: Break down the text into nodes representing different Markdown elements (e.g., code blocks, tables, headers).
  • markdownV2 Function: Processes nodes and uses MessageBuilder to construct messages according to the specified rules.
  • Formatting: The code is formatted using tabs, omits semicolons, and removes .js extensions in imports, adhering to the guidelines in FORMATTING.md.

This rewritten code ensures that the markdownV2 function returns a list of messages, each respecting the 4096-character limit, while preserving the Markdown structure and formatting as much as possible.

Share this Q&A