From 47adc6237668a379af4226b86710a0b383a41669 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Fri, 12 Dec 2025 21:03:12 -0500 Subject: [PATCH 1/6] Adding in new files --- src/commands/history/history.ts | 37 +++++++++++++++++----------- src/services/time/formatTimestamp.ts | 11 +++++++++ 2 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 src/services/time/formatTimestamp.ts diff --git a/src/commands/history/history.ts b/src/commands/history/history.ts index d2e69ff5..6b4b8a6c 100644 --- a/src/commands/history/history.ts +++ b/src/commands/history/history.ts @@ -7,8 +7,11 @@ import { /** * /history command - * Returns the last N raw user messages as a DM to the requester. - * MVP version for chat playback feature (F1). + * + * Returns the last N raw user messages from the current channel + * and delivers them privately via DM to the requester. + * + * This is the MVP implementation for chat playback (F1). */ export const data = new SlashCommandBuilder() .setName("history") @@ -21,25 +24,30 @@ export const data = new SlashCommandBuilder() .setMaxValue(50), ); +/** + * Execute the /history command. + */ export async function execute( interaction: ChatInputCommandInteraction, ): Promise { // Default to 50 messages if no count is provided. const count = interaction.options.getInteger("count") ?? 50; - // Ephemeral: only the caller sees the acknowledgment message. + // Acknowledge the command privately so only the caller sees status updates. await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - // Fetch messages from the current channel. - const messages = await interaction.channel?.messages.fetch({ limit: count }); - if (!messages) { - await interaction.editReply("Could not fetch messages for this channel."); + // Ensure the command is run in a text-based channel. + if (!interaction.channel || !interaction.channel.isTextBased()) { + await interaction.editReply("This channel does not support message history."); return; } + // Fetch recent messages from the channel. + const messages = await interaction.channel.messages.fetch({ limit: count }); + /** - * Filter user messages only (exclude bots), - * oldest first so playback reads correctly. + * Filter out bot messages and sort oldest → newest + * so the transcript reads naturally. */ const userMessages = messages .filter((m) => !m.author.bot && m.content) @@ -51,11 +59,11 @@ export async function execute( } /** - * Build a readable transcript. + * Build a readable transcript using 24-hour time. */ const text = userMessages .map((m) => { - const dt = new Date(m.createdTimestamp).toLocaleString([], { + const dt = new Date(m.createdTimestamp).toLocaleString("en-GB", { year: "numeric", month: "2-digit", day: "2-digit", @@ -69,7 +77,8 @@ export async function execute( .join("\n"); /** - * If too large for a normal DM, send as a file. + * If the transcript exceeds Discord’s 2000-character limit, + * send it as a text file instead. */ if (text.length > 2000) { const file = new AttachmentBuilder(Buffer.from(text, "utf8"), { @@ -94,7 +103,7 @@ export async function execute( } /** - * Normal-length transcript → DM it as text. + * Normal-length transcript → send as a DM message. */ try { await interaction.user.send(text); @@ -105,4 +114,4 @@ export async function execute( "I generated the history, but could not DM you. Your DMs may be closed.", ); } -} +} \ No newline at end of file diff --git a/src/services/time/formatTimestamp.ts b/src/services/time/formatTimestamp.ts new file mode 100644 index 00000000..0292fd23 --- /dev/null +++ b/src/services/time/formatTimestamp.ts @@ -0,0 +1,11 @@ +export function formatTimestamp(ts: number, tz: string): string { + return new Date(ts).toLocaleString("en-GB", { + timeZone: tz, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} From b3deb28ed2aa8de9ec6ccdaae010e0d60d68ac1f Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sat, 13 Dec 2025 17:21:48 -0500 Subject: [PATCH 2/6] Updating README.md to add in new file --- README.md | 13 ++-- src/commands/history/buildTranscript.ts | 90 +++++++++++++++++++++++++ src/services/time/formatTimestamp.ts | 19 ++++-- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/commands/history/buildTranscript.ts diff --git a/README.md b/README.md index 1a840d58..0e7f461f 100644 --- a/README.md +++ b/README.md @@ -115,14 +115,13 @@ OmegaBot is online ├── README.md ├── scripts │ └── precheck.sh -├── scripts copy -│ └── precheck.sh ├── src │ ├── bot.ts │ ├── commands │ │ ├── general │ │ │ └── ping.ts │ │ ├── history +│ │ │ ├── buildTranscript.ts │ │ │ └── history.ts │ │ └── summary │ │ └── summary.ts @@ -133,10 +132,12 @@ OmegaBot is online │ ├── discord │ │ ├── commandLoader.ts │ │ └── interactionHandler.ts -│ └── summary -│ ├── llmSummary.ts -│ ├── localSummary.ts -│ └── summarizer.ts +│ ├── summary +│ │ ├── llmSummary.ts +│ │ ├── localSummary.ts +│ │ └── summarizer.ts +│ └── time +│ └── formatTimestamp.ts └── tsconfig.json ``` diff --git a/src/commands/history/buildTranscript.ts b/src/commands/history/buildTranscript.ts new file mode 100644 index 00000000..91632ad0 --- /dev/null +++ b/src/commands/history/buildTranscript.ts @@ -0,0 +1,90 @@ +import { formatTimestamp } from "../../services/time/formatTimestamp.js"; + +/** + * Minimal shape required from a Discord message + * so this helper stays framework-agnostic. + */ +export type TranscriptMessage = { + createdTimestamp: number; + content: string; + author: { + username: string; + }; +}; + +export type TranscriptOptions = { + includeTimestamp: boolean; + includeAuthor: boolean; + maxLines?: number; + maxChars?: number; + timeZone?: string; + locale?: string; +}; + +export type TranscriptResult = { + text: string; + lineCount: number; + truncated: boolean; + tooLong: boolean; +}; + +/** + * Builds a readable transcript from a list of messages. + * Responsible ONLY for formatting + truncation rules. + */ +export function buildTranscript( + messages: TranscriptMessage[], + options: TranscriptOptions +): TranscriptResult { + const { + includeTimestamp, + includeAuthor, + maxLines, + maxChars, + timeZone = "UTC", + locale = "en-GB", + } = options; + + const lines: string[] = []; + let truncated = false; + let tooLong = false; + + for (const message of messages) { + if (!message?.content) continue; + + const parts: string[] = []; + + if (includeTimestamp) { + const ts = formatTimestamp(message.createdTimestamp, timeZone, locale); + parts.push(`[${ts}]`); + } + + if (includeAuthor) { + parts.push(`${message.author.username}:`); + } + + parts.push(message.content.trim()); + + const line = parts.join(" "); + lines.push(line); + + // Enforce maxLines + if (maxLines && lines.length >= maxLines) { + truncated = true; + break; + } + + // Enforce maxChars + if (maxChars && lines.join("\n").length >= maxChars) { + tooLong = true; + break; + } + } + + return { + text: lines.join("\n"), + lineCount: lines.length, + truncated, + tooLong, + }; +} \ No newline at end of file diff --git a/src/services/time/formatTimestamp.ts b/src/services/time/formatTimestamp.ts index 0292fd23..650d0cb2 100644 --- a/src/services/time/formatTimestamp.ts +++ b/src/services/time/formatTimestamp.ts @@ -1,6 +1,17 @@ -export function formatTimestamp(ts: number, tz: string): string { - return new Date(ts).toLocaleString("en-GB", { - timeZone: tz, +/** + * Format a Unix timestamp into a readable date/time string. + * + * - Uses 24-hour (military) time + * - Allows caller to control locale and timezone + * + * @param {number} ts - Unix timestamp (milliseconds) + * @param {string} locale - Locale string (ex: "en-GB", "en-US") + * @param {string} timeZone - IANA timezone (ex: "UTC", "America/New_York") + * @returns {string} + */ +export function formatTimestamp(ts: number, locale = "en-GB", timeZone = "UTC") { + return new Date(ts).toLocaleString(locale, { + timeZone, year: "numeric", month: "2-digit", day: "2-digit", @@ -8,4 +19,4 @@ export function formatTimestamp(ts: number, tz: string): string { minute: "2-digit", hour12: false, }); -} +} \ No newline at end of file From 78f38128b0bb742af51b6285c72015d14b828265 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 14 Dec 2025 13:39:56 -0500 Subject: [PATCH 3/6] Adding in more changes for F2 --- src/commands/history/history.ts | 169 ++++++++---------- .../transcript}/buildTranscript.ts | 2 +- 2 files changed, 72 insertions(+), 99 deletions(-) rename src/{commands/history => services/transcript}/buildTranscript.ts (95%) diff --git a/src/commands/history/history.ts b/src/commands/history/history.ts index 6b4b8a6c..b37cc64b 100644 --- a/src/commands/history/history.ts +++ b/src/commands/history/history.ts @@ -1,117 +1,90 @@ -import { - SlashCommandBuilder, - AttachmentBuilder, - MessageFlags, - type ChatInputCommandInteraction, -} from "discord.js"; +import { formatTimestamp } from "../time/formatTimestamp.js"; /** - * /history command - * - * Returns the last N raw user messages from the current channel - * and delivers them privately via DM to the requester. - * - * This is the MVP implementation for chat playback (F1). + * Minimal shape required from a Discord message + * so this helper stays framework-agnostic. */ -export const data = new SlashCommandBuilder() - .setName("history") - .setDescription("DMs you the most recent messages in this channel") - .addIntegerOption((opt) => - opt - .setName("count") - .setDescription("How many messages to fetch") - .setMinValue(5) - .setMaxValue(50), - ); +export type TranscriptMessage = { + createdTimestamp: number; + content: string; + author: { + username: string; + }; +}; -/** - * Execute the /history command. - */ -export async function execute( - interaction: ChatInputCommandInteraction, -): Promise { - // Default to 50 messages if no count is provided. - const count = interaction.options.getInteger("count") ?? 50; +export type TranscriptOptions = { + includeTimestamp: boolean; + includeAuthor: boolean; + maxLines?: number; + maxChars?: number; + timeZone?: string; + locale?: string; +}; - // Acknowledge the command privately so only the caller sees status updates. - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); +export type TranscriptResult = { + text: string; + lineCount: number; + truncated: boolean; + tooLong: boolean; +}; - // Ensure the command is run in a text-based channel. - if (!interaction.channel || !interaction.channel.isTextBased()) { - await interaction.editReply("This channel does not support message history."); - return; - } +/** + * Builds a readable transcript from a list of messages. + * Responsible ONLY for formatting + truncation rules. + */ +export function buildTranscript( + messages: TranscriptMessage[], + options: TranscriptOptions +): TranscriptResult { + const { + includeTimestamp, + includeAuthor, + maxLines, + maxChars, + timeZone = "UTC", + locale = "en-GB", + } = options; - // Fetch recent messages from the channel. - const messages = await interaction.channel.messages.fetch({ limit: count }); + const lines: string[] = []; + let truncated = false; + let tooLong = false; - /** - * Filter out bot messages and sort oldest → newest - * so the transcript reads naturally. - */ - const userMessages = messages - .filter((m) => !m.author.bot && m.content) - .sort((a, b) => a.createdTimestamp - b.createdTimestamp); + for (const message of messages) { + if (!message?.content) continue; - if (userMessages.size === 0) { - await interaction.editReply("No history found."); - return; - } + const parts: string[] = []; - /** - * Build a readable transcript using 24-hour time. - */ - const text = userMessages - .map((m) => { - const dt = new Date(m.createdTimestamp).toLocaleString("en-GB", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); + if (includeTimestamp) { + const ts = formatTimestamp(message.createdTimestamp, timeZone, locale); + parts.push(`[${ts}]`); + } - return `[${dt}] ${m.author.username}: ${m.content}`; - }) - .join("\n"); + if (includeAuthor) { + parts.push(`${message.author.username}:`); + } - /** - * If the transcript exceeds Discord’s 2000-character limit, - * send it as a text file instead. - */ - if (text.length > 2000) { - const file = new AttachmentBuilder(Buffer.from(text, "utf8"), { - name: "history.txt", - }); + parts.push(message.content.trim()); - try { - await interaction.user.send({ - content: "Here is your recent chat history:", - files: [file], - }); + const line = parts.join(" "); + lines.push(line); - await interaction.editReply("History sent to your DMs."); - } catch (err) { - console.error("[history] DM file send failed", err); - await interaction.editReply( - "I generated the history, but your DMs appear to be closed.", - ); + // Enforce maxLines + if (maxLines && lines.length >= maxLines) { + truncated = true; + break; } - return; + // Enforce maxChars + if (maxChars && lines.join("\n").length >= maxChars) { + tooLong = true; + break; + } } - /** - * Normal-length transcript → send as a DM message. - */ - try { - await interaction.user.send(text); - await interaction.editReply("History sent to your DMs."); - } catch (err) { - console.error("[history] DM text send failed", err); - await interaction.editReply( - "I generated the history, but could not DM you. Your DMs may be closed.", - ); - } + return { + text: lines.join("\n"), + lineCount: lines.length, + truncated, + tooLong, + }; } \ No newline at end of file diff --git a/src/commands/history/buildTranscript.ts b/src/services/transcript/buildTranscript.ts similarity index 95% rename from src/commands/history/buildTranscript.ts rename to src/services/transcript/buildTranscript.ts index 91632ad0..b37cc64b 100644 --- a/src/commands/history/buildTranscript.ts +++ b/src/services/transcript/buildTranscript.ts @@ -1,4 +1,4 @@ -import { formatTimestamp } from "../../services/time/formatTimestamp.js"; +import { formatTimestamp } from "../time/formatTimestamp.js"; /** * Minimal shape required from a Discord message From 2e544369ea9fb0d1bd00667a6713faeda9fd1112 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 14 Dec 2025 13:40:26 -0500 Subject: [PATCH 4/6] Adding in more changes for F2 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e7f461f..e475818a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ OmegaBot is online │ │ ├── general │ │ │ └── ping.ts │ │ ├── history -│ │ │ ├── buildTranscript.ts │ │ │ └── history.ts │ │ └── summary │ │ └── summary.ts @@ -136,8 +135,10 @@ OmegaBot is online │ │ ├── llmSummary.ts │ │ ├── localSummary.ts │ │ └── summarizer.ts -│ └── time -│ └── formatTimestamp.ts +│ ├── time +│ │ └── formatTimestamp.ts +│ └── transcript +│ └── buildTranscript.ts └── tsconfig.json ``` From 1686ad9031b75d534fcbe2b36a32c9c7f608f2cb Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 14 Dec 2025 13:42:30 -0500 Subject: [PATCH 5/6] Fixing import --- src/commands/history/history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/history/history.ts b/src/commands/history/history.ts index b37cc64b..91632ad0 100644 --- a/src/commands/history/history.ts +++ b/src/commands/history/history.ts @@ -1,4 +1,4 @@ -import { formatTimestamp } from "../time/formatTimestamp.js"; +import { formatTimestamp } from "../../services/time/formatTimestamp.js"; /** * Minimal shape required from a Discord message From 19360f7f7153cd249aa9edc6ad6af37d59b55374 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 14 Dec 2025 13:43:25 -0500 Subject: [PATCH 6/6] Running prettier --- src/commands/history/history.ts | 4 ++-- src/services/time/formatTimestamp.ts | 8 ++++++-- src/services/transcript/buildTranscript.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/commands/history/history.ts b/src/commands/history/history.ts index 91632ad0..70e3dc49 100644 --- a/src/commands/history/history.ts +++ b/src/commands/history/history.ts @@ -34,7 +34,7 @@ export type TranscriptResult = { */ export function buildTranscript( messages: TranscriptMessage[], - options: TranscriptOptions + options: TranscriptOptions, ): TranscriptResult { const { includeTimestamp, @@ -87,4 +87,4 @@ export function buildTranscript( truncated, tooLong, }; -} \ No newline at end of file +} diff --git a/src/services/time/formatTimestamp.ts b/src/services/time/formatTimestamp.ts index 650d0cb2..2f65974e 100644 --- a/src/services/time/formatTimestamp.ts +++ b/src/services/time/formatTimestamp.ts @@ -9,7 +9,11 @@ * @param {string} timeZone - IANA timezone (ex: "UTC", "America/New_York") * @returns {string} */ -export function formatTimestamp(ts: number, locale = "en-GB", timeZone = "UTC") { +export function formatTimestamp( + ts: number, + locale = "en-GB", + timeZone = "UTC", +) { return new Date(ts).toLocaleString(locale, { timeZone, year: "numeric", @@ -19,4 +23,4 @@ export function formatTimestamp(ts: number, locale = "en-GB", timeZone = "UTC") minute: "2-digit", hour12: false, }); -} \ No newline at end of file +} diff --git a/src/services/transcript/buildTranscript.ts b/src/services/transcript/buildTranscript.ts index b37cc64b..1f1c035b 100644 --- a/src/services/transcript/buildTranscript.ts +++ b/src/services/transcript/buildTranscript.ts @@ -34,7 +34,7 @@ export type TranscriptResult = { */ export function buildTranscript( messages: TranscriptMessage[], - options: TranscriptOptions + options: TranscriptOptions, ): TranscriptResult { const { includeTimestamp, @@ -87,4 +87,4 @@ export function buildTranscript( truncated, tooLong, }; -} \ No newline at end of file +}