Skip to content
Closed
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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ OmegaBot is online
├── README.md
├── scripts
│ └── precheck.sh
├── scripts copy
│ └── precheck.sh
├── src
│ ├── bot.ts
│ ├── commands
Expand All @@ -133,10 +131,14 @@ 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
│ └── transcript
│ └── buildTranscript.ts
└── tsconfig.json
```

Expand Down
162 changes: 72 additions & 90 deletions src/commands/history/history.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,90 @@
import {
SlashCommandBuilder,
AttachmentBuilder,
MessageFlags,
type ChatInputCommandInteraction,
} from "discord.js";
import { formatTimestamp } from "../../services/time/formatTimestamp.js";

/**
* /history command
* Returns the last N raw user messages as a DM to the requester.
* MVP version for chat playback feature (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;
};
};

export async function execute(
interaction: ChatInputCommandInteraction,
): Promise<void> {
// 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;
};

// Ephemeral: only the caller sees the acknowledgment message.
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
export type TranscriptResult = {
text: string;
lineCount: number;
truncated: boolean;
tooLong: boolean;
};

// 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.");
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;

/**
* Filter user messages only (exclude bots),
* oldest first so playback reads correctly.
*/
const userMessages = messages
.filter((m) => !m.author.bot && m.content)
.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
const lines: string[] = [];
let truncated = false;
let tooLong = false;

if (userMessages.size === 0) {
await interaction.editReply("No history found.");
return;
}
for (const message of messages) {
if (!message?.content) continue;

/**
* Build a readable transcript.
*/
const text = userMessages
.map((m) => {
const dt = new Date(m.createdTimestamp).toLocaleString([], {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
const parts: string[] = [];

return `[${dt}] ${m.author.username}: ${m.content}`;
})
.join("\n");
if (includeTimestamp) {
const ts = formatTimestamp(message.createdTimestamp, timeZone, locale);
parts.push(`[${ts}]`);
}

/**
* If too large for a normal DM, send as a file.
*/
if (text.length > 2000) {
const file = new AttachmentBuilder(Buffer.from(text, "utf8"), {
name: "history.txt",
});
if (includeAuthor) {
parts.push(`${message.author.username}:`);
}

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 → DM it as text.
*/
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,
};
}
26 changes: 26 additions & 0 deletions src/services/time/formatTimestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
90 changes: 90 additions & 0 deletions src/services/transcript/buildTranscript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { formatTimestamp } from "../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,
};
}