Skip to content

Commit

Permalink
Merge pull request #21 from ubq-testing/gh-storage
Browse files Browse the repository at this point in the history
More logs
  • Loading branch information
Keyrxng authored Oct 29, 2024
2 parents 224d9d0 + 1f3815b commit a01828b
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 120 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"userbase",
"Superbase",
"SUPABASE",
"CODEOWNER"
"CODEOWNER",
"nosniff"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@
"@commitlint/config-conventional"
]
}
}
}
21 changes: 4 additions & 17 deletions src/bot/helpers/grammy-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Dependencies {
}

interface ExtendedContextFlavor extends Dependencies {
adapters?: ReturnType<typeof createAdapters>;
adapters: ReturnType<typeof createAdapters>;
}

export type GrammyContext = ParseModeFlavor<HydrateFlavor<DefaultContext & ExtendedContextFlavor & SessionFlavor<SessionData> & AutoChatActionFlavor>>;
Expand All @@ -44,31 +44,18 @@ export async function createContextConstructor({ logger, config, octokit }: Depe
logger: Logger;
octokit: RestOctokitFromApp = octokit;
config: UbiquityOsContext["env"];
adapters: ReturnType<typeof createAdapters> | undefined = adapters;
adapters: ReturnType<typeof createAdapters>;

constructor(update: GrammyTelegramUpdate, api: Api, me: UserFromGetMe) {
super(update, api, me);
this.logger = logger;
this.config = config;

if (!this.adapters) {
if (!adapters) {
throw new Error("Adapters not initialized");
}

/**
* We'll need to add handling to detect forks and in such cases
* we'll need to handle the storage differently.
*
* Storing the repository full name would work, and we already have it
* during setup. Otherwise via plugin config.
*
* if (me.username !== "ubiquity_os_bot") { }
*/

/**
* We only operate as one organization on telegram, so I'm assuming
* that we'll be centralizing the storage obtained.
*/
this.adapters = adapters;
}
} as unknown as new (update: GrammyTelegramUpdate, api: Api, me: UserFromGetMe) => GrammyContext;
}
90 changes: 55 additions & 35 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import type { BotConfig, StorageAdapter } from "grammy";
import { Bot as TelegramBot } from "grammy";
import { Context as UbiquityOsContext } from "../types";
import { BotConfig, StorageAdapter } from "grammy";
import { Bot as TelegramBot, Context as GrammyContext } from "grammy";
import { autoChatAction } from "@grammyjs/auto-chat-action";
import { hydrate } from "@grammyjs/hydrate";
import { hydrateReply, parseMode } from "@grammyjs/parse-mode";
import { Octokit as RestOctokitFromApp } from "octokit";

import { Logger } from "../utils/logger";
import { createContextConstructor, SessionData } from "./helpers/grammy-context";
import { errorHandler } from "./handlers/error";

import { adminFeature } from "./features/admin/admin";
import { setWebhookFeature } from "./features/admin/set-webhook";
import { userIdFeature } from "./features/commands/private-chat/user-id";
import { chatIdFeature } from "./features/commands/shared/chat-id";
import { botIdFeature } from "./features/commands/private-chat/bot-id";
import { banCommand } from "./features/commands/groups/ban";
import { setWebhookFeature } from "./features/admin/set-webhook";
import { Logger } from "../utils/logger";
import { createContextConstructor, GrammyContext, SessionData } from "./helpers/grammy-context";
import { errorHandler } from "./handlers/error";
import { session } from "./middlewares/session";
import { welcomeFeature } from "./features/start-command";
import { unhandledFeature } from "./features/helpers/unhandled";
import { registerFeature } from "./features/commands/private-chat/register";
import { notifySubscribeFeature } from "./features/commands/private-chat/notify-subscribe";
import { walletFeature } from "./features/commands/private-chat/wallet";
import { Octokit as RestOctokitFromApp } from "octokit";
import { banCommand } from "./features/commands/groups/ban";
import { welcomeFeature } from "./features/start-command";
import { unhandledFeature } from "./features/helpers/unhandled";
import { Context } from "../types";
import { session } from "./middlewares/session";

interface Dependencies {
config: UbiquityOsContext["env"];
config: Context["env"];
logger: Logger;
octokit: RestOctokitFromApp;
}
Expand All @@ -37,42 +39,60 @@ function getSessionKey(ctx: Omit<GrammyContext, "session">) {
}

export async function createBot(token: string, dependencies: Dependencies, options: Options = {}) {
const { logger } = dependencies;

const bot = new TelegramBot(token, {
...options.botConfig,
ContextConstructor: await createContextConstructor(dependencies),
});
const protectedBot = bot.errorBoundary(errorHandler);

// Error handling
bot.catch(errorHandler);

// Configure bot API
bot.api.config.use(parseMode("HTML"));

protectedBot.use(autoChatAction(bot.api));
protectedBot.use(hydrateReply);
protectedBot.use(hydrate());
protectedBot.use(session({ getSessionKey, storage: options.botSessionStorage }));
// Middleware usage
bot.use(hydrate());
bot.use(hydrateReply);
bot.use(autoChatAction());

// Session middleware
bot.use(
session({
getSessionKey,
storage: options.botSessionStorage,
})
);

// the `/start` command for a traditional TG bot, doubt we need this as-is
// but a variation of can be built for various scenarios.
protectedBot.use(welcomeFeature);
// Log middleware initialization
logger.info("Initializing middlewares and features...");

// admin commands
protectedBot.use(adminFeature);
protectedBot.use(setWebhookFeature);
// Feature middlewares
bot.use(welcomeFeature);

// development commands
protectedBot.use(userIdFeature);
protectedBot.use(chatIdFeature);
protectedBot.use(botIdFeature);
// Admin commands
bot.use(adminFeature);
bot.use(setWebhookFeature);

// Development commands
bot.use(userIdFeature);
bot.use(chatIdFeature);
bot.use(botIdFeature);

// Private chat commands
protectedBot.use(registerFeature); // /register <GitHub username>
protectedBot.use(notifySubscribeFeature); // /subscribe
protectedBot.use(walletFeature); // /wallet <wallet address>
bot.use(registerFeature);
bot.use(notifySubscribeFeature);
bot.use(walletFeature);

// Group commands
bot.use(banCommand);

// group commands
protectedBot.use(banCommand);
// Unhandled command handler
bot.use(unhandledFeature);

// unhandled command handler
protectedBot.use(unhandledFeature);
// Log bot is ready
logger.info("Bot is initialized and ready to handle updates.");

return bot;
}
Expand Down
4 changes: 0 additions & 4 deletions src/bot/setcommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,6 @@ function getPrivateChatCommands(): BotCommand[] {
command: "wallet",
description: "Register your wallet address",
},
{
command: "setcommands",
description: "Set the bot's commands",
},
];
}

Expand Down
139 changes: 132 additions & 7 deletions src/handlers/telegram-webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,145 @@ import { TelegramBotSingleton } from "../types/telegram-bot-single";
import { logger } from "../utils/logger";

export async function handleTelegramWebhook(request: Request, env: Env): Promise<Response> {
let server;
const failures: unknown[] = [];
logger.info("Handling telegram webhook request", { request });

// Initialize bot instance
const botInstance = await initializeBotInstance(env, failures);

// Get server and bot from botInstance
const { server, bot } = getServerFromBot(botInstance, failures);

// Make server request even if server is null to collect all failures
const res = await makeServerRequest(server, request, env, failures);

// Read response body even if res is null to collect all failures
const body = await readResponseBody(res, failures);

// Create final response
const response = createResponse(res, body, failures);

// Try to send error messages if any
await sendErrorMessages(bot, env, failures);

return response;
}

async function initializeBotInstance(env: Env, failures: unknown[]) {
try {
const botInstance = await TelegramBotSingleton.initialize(env);
logger.info("Initialized TelegramBotSingleton");
return botInstance;
} catch (er) {
const errorInfo = {
message: "Error initializing TelegramBotSingleton",
error: er instanceof Error ? er.message : String(er),
stack: er instanceof Error ? er.stack : undefined,
};
failures.push(errorInfo);
logger.error(errorInfo.message, { error: er });
return null;
}
}

function getServerFromBot(botInstance: TelegramBotSingleton | null, failures: unknown[]) {
try {
server = (await TelegramBotSingleton.initialize(env)).getServer();
logger.info("Getting server from bot");
const server = botInstance?.getServer();
const bot = botInstance?.getBot();
if (!server || !bot) {
throw new Error("Server or bot is undefined");
}
logger.info("Got server from bot");
return { server, bot };
} catch (er) {
logger.error("Error initializing TelegramBotSingleton", { er });
return new Response("Error initializing TelegramBotSingleton", { status: 500, statusText: "Internal Server Error" });
const errorInfo = {
message: "Error getting server from bot",
error: er instanceof Error ? er.message : String(er),
stack: er instanceof Error ? er.stack : undefined,
};
failures.push(errorInfo);
logger.error(errorInfo.message, { error: er });
return { server: null, bot: null };
}
}

async function makeServerRequest(
server: ReturnType<TelegramBotSingleton["getServer"]> | null,
request: Request,
env: Env,
failures: unknown[]
): Promise<Response> {
try {
if (!server) {
throw new Error("Server is null");
}
logger.info("Making hono server request");
const res = await server.fetch(request, env);
logger.info("Response from TelegramBotSingleton", { res });
logger.info("Hono server request made", { res });
return res;
} catch (er) {
logger.error("Error fetching request from hono server", { er });
return new Response("Error fetching request from hono server", { status: 500, statusText: "Internal Server Error" });
const errorInfo = {
message: "Error fetching request from hono server",
error: er instanceof Error ? er.message : String(er),
stack: er instanceof Error ? er.stack : undefined,
};
failures.push(errorInfo);
logger.error(errorInfo.message, { error: er });
return new Response("Internal Server Error", { status: 500 });
}
}

async function readResponseBody(res: Response, failures: unknown[]): Promise<string> {
let body;
try {
body = await res.text();
} catch (er) {
logger.error("Error reading .text() from hono server", { er });
}

try {
logger.info("Response from hono server", { body });
return typeof body === "string" ? body : JSON.stringify(body);
} catch (er) {
const errorInfo = {
message: "Error reading response from hono server",
error: er instanceof Error ? er.message : String(er),
stack: er instanceof Error ? er.stack : undefined,
};
failures.push(errorInfo);
logger.error(errorInfo.message, { error: er });
return "";
}
}

function createResponse(res: Response, body: string, failures: unknown[]): Response {
try {
if (!res) {
throw new Error("Response is null");
}
const { status, statusText, headers } = res;
logger.info("Creating response from hono server", { status, statusText, headers });
return new Response(body, { status, statusText, headers });
} catch (er) {
const errorInfo = {
message: "Error creating response from hono server",
error: er instanceof Error ? er.message : String(er),
stack: er instanceof Error ? er.stack : undefined,
};
failures.push(errorInfo);
logger.error(errorInfo.message, { error: er });
return new Response("Internal Server Error", { status: 500 });
}
}

async function sendErrorMessages(bot: ReturnType<TelegramBotSingleton["getBot"]> | null, env: Env, failures: unknown[]): Promise<void> {
if (failures.length) {
const errorMessage = failures.map((failure, index) => `Error ${index + 1}:\n${JSON.stringify(failure, null, 2)}`).join("\n\n");
try {
await bot?.api.sendMessage(env.TELEGRAM_BOT_ENV.botSettings.TELEGRAM_BOT_ADMINS[0], `Error handling webhook request:\n\n${errorMessage}`);
} catch (er) {
logger.error("Error sending error message to admin", { er });
}
}
}
Loading

0 comments on commit a01828b

Please sign in to comment.