diff --git a/apps/bridge-deltachat/package.json b/apps/bridge-deltachat/package.json index 3f76524..9960ef2 100644 --- a/apps/bridge-deltachat/package.json +++ b/apps/bridge-deltachat/package.json @@ -7,10 +7,8 @@ "dependencies": { "@deltachat/jsonrpc-client": "^1.151.1", "@deltachat/stdio-rpc-server": "^1.151.1", - "@hapi/hapi": "^21.4.3", - "@hapipal/schmervice": "^3.0.0", - "@hapipal/toys": "^4.0.0", - "hapi-pino": "^13.0.0", + "@hono/node-server": "^1.13.8", + "hono": "^4.7.4", "pino": "^9.6.0", "pino-pretty": "^13.0.0" }, diff --git a/apps/bridge-deltachat/src/index.ts b/apps/bridge-deltachat/src/index.ts index 78a4627..c8841e9 100644 --- a/apps/bridge-deltachat/src/index.ts +++ b/apps/bridge-deltachat/src/index.ts @@ -1,39 +1,29 @@ -import * as Hapi from "@hapi/hapi"; -import hapiPino from "hapi-pino"; -import Schmervice from "@hapipal/schmervice"; +import { serve } from "@hono/node-server"; import DeltaChatService from "./service.ts"; -import { - ConfigureBotRoute, - GetBotRoute, - SendMessageRoute, - UnconfigureBotRoute, - HealthRoute, -} from "./routes.ts"; +import { createRoutes } from "./routes.ts"; import { createLogger } from "./lib/logger"; const logger = createLogger("bridge-deltachat-index"); -const server = Hapi.server({ port: 5001 }); - -const startServer = async () => { - await server.register({ plugin: hapiPino }); - - server.route(ConfigureBotRoute); - server.route(GetBotRoute); - server.route(SendMessageRoute); - server.route(UnconfigureBotRoute); - server.route(HealthRoute); - - await server.register(Schmervice); - server.registerService(DeltaChatService); - - await server.start(); - - return server; -}; - const main = async () => { - await startServer(); + const service = new DeltaChatService(); + await service.initialize(); + + const app = createRoutes(service); + const port = parseInt(process.env.PORT || "5001", 10); + + serve({ fetch: app.fetch, port }, (info) => { + logger.info({ port: info.port }, "bridge-deltachat listening"); + }); + + const shutdown = async () => { + logger.info("Shutting down..."); + await service.teardown(); + process.exit(0); + }; + + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); }; main().catch((err) => { diff --git a/apps/bridge-deltachat/src/routes.ts b/apps/bridge-deltachat/src/routes.ts index 696e171..3480d50 100644 --- a/apps/bridge-deltachat/src/routes.ts +++ b/apps/bridge-deltachat/src/routes.ts @@ -1,111 +1,54 @@ -import * as Hapi from "@hapi/hapi"; -import Toys from "@hapipal/toys"; -import DeltaChatService from "./service.ts"; +import { Hono } from "hono"; +import type DeltaChatService from "./service.ts"; +import { createLogger } from "./lib/logger"; -const withDefaults = Toys.withRouteDefaults({ - options: { - cors: true, - }, -}); +const logger = createLogger("bridge-deltachat-routes"); -const getService = (request: Hapi.Request): DeltaChatService => { - const { deltaChatService } = request.services(); +export function createRoutes(service: DeltaChatService): Hono { + const app = new Hono(); - return deltaChatService as DeltaChatService; -}; + app.post("/api/bots/:id/configure", async (c) => { + const id = c.req.param("id"); + const { email, password } = await c.req.json<{ email: string; password: string }>(); -interface ConfigureRequest { - email: string; - password: string; + try { + const result = await service.configure(id, email, password); + logger.info({ id, email }, "Bot configured"); + return c.json(result); + } catch (err: any) { + logger.error({ id, error: err.message }, "Failed to configure bot"); + return c.json({ error: err.message }, 500); + } + }); + + app.get("/api/bots/:id", async (c) => { + const id = c.req.param("id"); + return c.json(await service.getBot(id)); + }); + + app.post("/api/bots/:id/send", async (c) => { + const id = c.req.param("id"); + const { email, message, attachments } = await c.req.json<{ + email: string; + message: string; + attachments?: Array<{ data: string; filename: string; mime_type: string }>; + }>(); + + const result = await service.send(id, email, message, attachments); + logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); + return c.json({ result }); + }); + + app.post("/api/bots/:id/unconfigure", async (c) => { + const id = c.req.param("id"); + await service.unconfigure(id); + logger.info({ id }, "Bot unconfigured"); + return c.body(null, 200); + }); + + app.get("/api/health", (c) => { + return c.json({ status: "ok" }); + }); + + return app; } - -interface SendMessageRequest { - email: string; - message: string; - attachments?: Array<{ data: string; filename: string; mime_type: string }>; -} - -export const ConfigureBotRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/configure", - options: { - description: "Configure a bot with email credentials", - async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const { id } = request.params; - const { email, password } = request.payload as ConfigureRequest; - const service = getService(request); - - try { - const result = await service.configure(id, email, password); - request.logger.info({ id, email }, "Bot configured at %s", new Date().toISOString()); - return h.response(result).code(200); - } catch (err: any) { - request.logger.error({ id, error: err.message }, "Failed to configure bot"); - return h.response({ error: err.message }).code(500); - } - }, - }, -}); - -export const GetBotRoute = withDefaults({ - method: "get", - path: "/api/bots/{id}", - options: { - description: "Get bot status", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const service = getService(request); - return service.getBot(id); - }, - }, -}); - -export const SendMessageRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/send", - options: { - description: "Send a message", - async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const { id } = request.params; - const { email, message, attachments } = request.payload as SendMessageRequest; - const service = getService(request); - - const result = await service.send(id, email, message, attachments); - request.logger.info( - { id, attachmentCount: attachments?.length || 0 }, - "Sent a message at %s", - new Date().toISOString(), - ); - - return h.response({ result }).code(200); - }, - }, -}); - -export const UnconfigureBotRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/unconfigure", - options: { - description: "Unconfigure and remove a bot", - async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const { id } = request.params; - const service = getService(request); - - await service.unconfigure(id); - request.logger.info({ id }, "Bot unconfigured at %s", new Date().toISOString()); - - return h.response().code(200); - }, - }, -}); - -export const HealthRoute = withDefaults({ - method: "get", - path: "/api/health", - options: { - description: "Health check", - async handler(_request: Hapi.Request, h: Hapi.ResponseToolkit) { - return h.response({ status: "ok" }).code(200); - }, - }, -}); diff --git a/apps/bridge-deltachat/src/service.ts b/apps/bridge-deltachat/src/service.ts index c2b214b..7dca25c 100644 --- a/apps/bridge-deltachat/src/service.ts +++ b/apps/bridge-deltachat/src/service.ts @@ -1,5 +1,3 @@ -import { Server } from "@hapi/hapi"; -import { Service } from "@hapipal/schmervice"; import { startDeltaChat, DeltaChat } from "@deltachat/stdio-rpc-server"; import fs from "fs"; import path from "path"; @@ -17,14 +15,13 @@ interface BotMapping { [botId: string]: number; } -export default class DeltaChatService extends Service { +export default class DeltaChatService { private dc: DeltaChat | null = null; private botMapping: BotMapping = {}; private dataDir: string; private mappingFile: string; - constructor(server: Server, options: never) { - super(server, options); + constructor() { this.dataDir = process.env.DELTACHAT_DATA_DIR || "/home/node/deltachat-data"; this.mappingFile = path.join(this.dataDir, "bot-mapping.json"); } diff --git a/apps/bridge-deltachat/src/types.ts b/apps/bridge-deltachat/src/types.ts deleted file mode 100644 index ccf27c9..0000000 --- a/apps/bridge-deltachat/src/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type DeltaChatService from "./service.ts"; - -declare module "@hapipal/schmervice" { - interface SchmerviceDecorator { - (namespace: "deltachat"): DeltaChatService; - } - type ServiceFunctionalInterface = { name: string }; -} diff --git a/apps/bridge-whatsapp/package.json b/apps/bridge-whatsapp/package.json index 7d62ce4..7632b63 100644 --- a/apps/bridge-whatsapp/package.json +++ b/apps/bridge-whatsapp/package.json @@ -6,11 +6,9 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@adiwajshing/keyed-db": "0.2.4", - "@hapi/hapi": "^21.4.3", - "@hapipal/schmervice": "^3.0.0", - "@hapipal/toys": "^4.0.0", + "@hono/node-server": "^1.13.8", "@whiskeysockets/baileys": "6.7.21", - "hapi-pino": "^13.0.0", + "hono": "^4.7.4", "link-preview-js": "^3.1.0", "pino": "^9.6.0", "pino-pretty": "^13.0.0" diff --git a/apps/bridge-whatsapp/src/index.ts b/apps/bridge-whatsapp/src/index.ts index a84050a..d66f037 100644 --- a/apps/bridge-whatsapp/src/index.ts +++ b/apps/bridge-whatsapp/src/index.ts @@ -1,39 +1,29 @@ -import * as Hapi from "@hapi/hapi"; -import hapiPino from "hapi-pino"; -import Schmervice from "@hapipal/schmervice"; +import { serve } from "@hono/node-server"; import WhatsappService from "./service.ts"; -import { - RegisterBotRoute, - UnverifyBotRoute, - GetBotRoute, - SendMessageRoute, - ReceiveMessageRoute, -} from "./routes.ts"; +import { createRoutes } from "./routes.ts"; import { createLogger } from "./lib/logger"; const logger = createLogger("bridge-whatsapp-index"); -const server = Hapi.server({ port: 5000 }); - -const startServer = async () => { - await server.register({ plugin: hapiPino }); - - server.route(RegisterBotRoute); - server.route(UnverifyBotRoute); - server.route(GetBotRoute); - server.route(SendMessageRoute); - server.route(ReceiveMessageRoute); - - await server.register(Schmervice); - server.registerService(WhatsappService); - - await server.start(); - - return server; -}; - const main = async () => { - await startServer(); + const service = new WhatsappService(); + await service.initialize(); + + const app = createRoutes(service); + const port = parseInt(process.env.PORT || "5000", 10); + + serve({ fetch: app.fetch, port }, (info) => { + logger.info({ port: info.port }, "bridge-whatsapp listening"); + }); + + const shutdown = async () => { + logger.info("Shutting down..."); + await service.teardown(); + process.exit(0); + }; + + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); }; main().catch((err) => { diff --git a/apps/bridge-whatsapp/src/routes.ts b/apps/bridge-whatsapp/src/routes.ts index 7315020..d70df22 100644 --- a/apps/bridge-whatsapp/src/routes.ts +++ b/apps/bridge-whatsapp/src/routes.ts @@ -1,125 +1,58 @@ -import * as Hapi from "@hapi/hapi"; -import Toys from "@hapipal/toys"; -import WhatsappService from "./service.ts"; +import { Hono } from "hono"; +import type WhatsappService from "./service.ts"; +import { createLogger } from "./lib/logger"; -const withDefaults = Toys.withRouteDefaults({ - options: { - cors: true, - }, -}); +const logger = createLogger("bridge-whatsapp-routes"); -const getService = (request: Hapi.Request): WhatsappService => { - const { whatsappService } = request.services(); +export function createRoutes(service: WhatsappService): Hono { + const app = new Hono(); - return whatsappService as WhatsappService; -}; + app.post("/api/bots/:id/send", async (c) => { + const id = c.req.param("id"); + const { phoneNumber, message, attachments } = await c.req.json<{ + phoneNumber: string; + message: string; + attachments?: Array<{ data: string; filename: string; mime_type: string }>; + }>(); -interface MessageRequest { - phoneNumber: string; - message: string; - attachments?: Array<{ data: string; filename: string; mime_type: string }>; + await service.send(id, phoneNumber, message, attachments); + logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); + + return c.json({ + result: { + recipient: phoneNumber, + timestamp: new Date().toISOString(), + source: id, + }, + }); + }); + + app.get("/api/bots/:id/receive", async (c) => { + const id = c.req.param("id"); + const date = new Date(); + const twoDaysAgo = new Date(date.getTime()); + twoDaysAgo.setDate(date.getDate() - 2); + + const messages = await service.receive(id, twoDaysAgo); + return c.json(messages); + }); + + app.post("/api/bots/:id/register", async (c) => { + const id = c.req.param("id"); + await service.register(id); + return c.body(null, 200); + }); + + app.post("/api/bots/:id/unverify", async (c) => { + const id = c.req.param("id"); + await service.unverify(id); + return c.body(null, 200); + }); + + app.get("/api/bots/:id", async (c) => { + const id = c.req.param("id"); + return c.json(service.getBot(id)); + }); + + return app; } - -export const SendMessageRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/send", - options: { - description: "Send a message", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const { phoneNumber, message, attachments } = request.payload as MessageRequest; - const whatsappService = getService(request); - await whatsappService.send(id, phoneNumber, message as string, attachments); - request.logger.info( - { - id, - attachmentCount: attachments?.length || 0, - }, - "Sent a message at %s", - new Date().toISOString(), - ); - - return _h - .response({ - result: { - recipient: phoneNumber, - timestamp: new Date().toISOString(), - source: id, - }, - }) - .code(200); - }, - }, -}); - -export const ReceiveMessageRoute = withDefaults({ - method: "get", - path: "/api/bots/{id}/receive", - options: { - description: "Receive messages", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const whatsappService = getService(request); - const date = new Date(); - const twoDaysAgo = new Date(date.getTime()); - twoDaysAgo.setDate(date.getDate() - 2); - request.logger.info({ id }, "Received messages at %s", new Date().toISOString()); - - return whatsappService.receive(id, twoDaysAgo); - }, - }, -}); - -export const RegisterBotRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/register", - options: { - description: "Register a bot", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const whatsappService = getService(request); - - await whatsappService.register(id); - /* - , (error: string) => { - if (error) { - return _h.response(error).code(500); - } - request.logger.info({ id }, "Register bot at %s", new Date()); - - return _h.response().code(200); - }); - */ - - return _h.response().code(200); - }, - }, -}); - -export const UnverifyBotRoute = withDefaults({ - method: "post", - path: "/api/bots/{id}/unverify", - options: { - description: "Unverify bot", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const whatsappService = getService(request); - - return whatsappService.unverify(id); - }, - }, -}); - -export const GetBotRoute = withDefaults({ - method: "get", - path: "/api/bots/{id}", - options: { - description: "Get bot info", - async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { - const { id } = request.params; - const whatsappService = getService(request); - - return whatsappService.getBot(id); - }, - }, -}); diff --git a/apps/bridge-whatsapp/src/service.ts b/apps/bridge-whatsapp/src/service.ts index 6ffd147..a311cfc 100644 --- a/apps/bridge-whatsapp/src/service.ts +++ b/apps/bridge-whatsapp/src/service.ts @@ -1,5 +1,3 @@ -import { Server } from "@hapi/hapi"; -import { Service } from "@hapipal/schmervice"; import makeWASocket, { DisconnectReason, proto, @@ -23,16 +21,12 @@ const logger = createLogger("bridge-whatsapp-service"); export type AuthCompleteCallback = (error?: string) => void; -export default class WhatsappService extends Service { +export default class WhatsappService { connections: { [key: string]: any } = {}; loginConnections: { [key: string]: any } = {}; static browserDescription: [string, string, string] = ["Bridge", "Chrome", "2.0"]; - constructor(server: Server, options: never) { - super(server, options); - } - getBaseDirectory(): string { return `/home/node/baileys`; } @@ -87,7 +81,6 @@ export default class WhatsappService extends Service { private async createConnection( botID: string, - server: Server, options: any, authCompleteCallback?: any, ) { @@ -125,13 +118,13 @@ export default class WhatsappService extends Service { const disconnectStatusCode = (lastDisconnect?.error as any)?.output?.statusCode; if (disconnectStatusCode === DisconnectReason.restartRequired) { logger.info("reconnecting after got new login"); - await this.createConnection(botID, server, options); + await this.createConnection(botID, options); authCompleteCallback?.(); } else if (disconnectStatusCode !== DisconnectReason.loggedOut) { logger.info("reconnecting"); await this.sleep(pause); pause *= 2; - this.createConnection(botID, server, options); + this.createConnection(botID, options); } } } @@ -178,7 +171,7 @@ export default class WhatsappService extends Service { const { version, isLatest } = await fetchLatestBaileysVersion(); logger.info({ version: version.join("."), isLatest }, "using WA version"); - await this.createConnection(botID, this.server, { + await this.createConnection(botID, { browser: WhatsappService.browserDescription, version, }); @@ -355,7 +348,6 @@ export default class WhatsappService extends Service { const { version } = await fetchLatestBaileysVersion(); await this.createConnection( botID, - this.server, { version, browser: WhatsappService.browserDescription }, callback, ); @@ -452,10 +444,6 @@ export default class WhatsappService extends Service { _botID: string, _lastReceivedDate: Date, ): Promise { - // loadAllUnreadMessages() was removed in Baileys 7.x - // Messages are now delivered via events (messages.upsert, messaging-history.set) - // and forwarded to webhooks automatically. - // See: https://baileys.wiki/docs/migration/to-v7.0.0/ throw new Error( "Message polling is no longer supported in Baileys 7.x. " + "Please configure a webhook to receive messages instead. " + diff --git a/apps/bridge-whatsapp/src/types.ts b/apps/bridge-whatsapp/src/types.ts deleted file mode 100644 index dc6d4fb..0000000 --- a/apps/bridge-whatsapp/src/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type WhatsappService from "./service.ts"; - -declare module "@hapipal/schmervice" { - interface SchmerviceDecorator { - (namespace: "whatsapp"): WhatsappService; - } - type ServiceFunctionalInterface = { name: string }; -}