1import { AtpAgent, AtpAgentOptions } from "@atproto/api";
2import { Logger } from "../utils/logger";
3import type { Bot } from "../types/bot";
4
5/**
6 * Base class for all bot agents with common functionality.
7 * Provides correlation tracking and structured logging capabilities.
8 */
9export abstract class BotAgent extends AtpAgent {
10 protected currentCorrelationId: string | null = null;
11 protected operationStartTime: number | null = null;
12
13 constructor(
14 public opts: AtpAgentOptions,
15 protected bot: Bot
16 ) {
17 super(opts);
18 }
19
20 /**
21 * Start tracking an operation with correlation ID and timing.
22 * @protected
23 */
24 protected startOperationTracking(): void {
25 this.currentCorrelationId = Logger.generateCorrelationId();
26 this.operationStartTime = Date.now();
27 }
28
29 /**
30 * Clear operation tracking state.
31 * @protected
32 */
33 protected clearOperationTracking(): void {
34 this.currentCorrelationId = null;
35 this.operationStartTime = null;
36 }
37
38 /**
39 * Get the bot identifier for logging purposes.
40 * @protected
41 */
42 protected getBotId(): string {
43 return this.bot.username || this.bot.identifier;
44 }
45
46 /**
47 * Log a message with correlation ID during bot execution.
48 * Call this from within your bot methods to log with proper correlation tracking.
49 */
50 logAction(
51 level: "info" | "warn" | "error",
52 message: string,
53 additionalContext?: Record<string, unknown>
54 ): void {
55 const logContext: Record<string, unknown> = {
56 botId: this.getBotId(),
57 ...additionalContext,
58 };
59
60 if (this.currentCorrelationId && this.operationStartTime) {
61 logContext.correlationId = this.currentCorrelationId;
62 logContext.operation = this.getOperationName();
63 logContext.duration = `${Date.now() - this.operationStartTime}ms`;
64 }
65
66 switch (level) {
67 case "info":
68 Logger.info(message, logContext);
69 break;
70 case "warn":
71 Logger.warn(message, logContext);
72 break;
73 case "error":
74 Logger.error(message, logContext);
75 break;
76 }
77 }
78
79 /**
80 * Get the operation name for logging. Override in subclasses.
81 * @protected
82 */
83 protected abstract getOperationName(): string;
84}
85
86/**
87 * Generic bot initialization function that handles common setup.
88 */
89export async function initializeBotAgent<T extends BotAgent>(
90 botType: string,
91 bot: Bot,
92 createAgent: (opts: AtpAgentOptions, bot: Bot) => T
93): Promise<T | null> {
94 const botId = bot.username ?? bot.identifier;
95 const correlationId = Logger.startOperation(`initialize${botType}`, { botId });
96 const startTime = Date.now();
97
98 const agent = createAgent({ service: bot.service }, bot);
99
100 try {
101 Logger.info(`Initializing ${botType.toLowerCase()}`, { correlationId, botId });
102
103 const login = await agent.login({
104 identifier: bot.identifier,
105 password: bot.password!,
106 });
107
108 if (!login.success) {
109 Logger.warn(`${botType} login failed`, { correlationId, botId });
110 return null;
111 }
112
113 Logger.endOperation(`initialize${botType}`, startTime, { correlationId, botId });
114 return agent;
115 } catch (error) {
116 Logger.error(`Failed to initialize ${botType.toLowerCase()}`, {
117 correlationId,
118 botId,
119 error: error instanceof Error ? error.message : String(error),
120 duration: Date.now() - startTime,
121 });
122 return null;
123 }
124}