@theowlops/channelhub 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/bin/cli.ts +36 -0
  4. package/dist/bridges/mcp/index.d.ts +14 -0
  5. package/dist/bridges/mcp/index.js +96 -0
  6. package/dist/bridges/webhook/index.d.ts +32 -0
  7. package/dist/bridges/webhook/index.js +123 -0
  8. package/dist/channels/discord/adapter.d.ts +24 -0
  9. package/dist/channels/discord/index.d.ts +1 -0
  10. package/dist/channels/discord/index.js +130 -0
  11. package/dist/channels/slack/adapter.d.ts +20 -0
  12. package/dist/channels/slack/index.d.ts +1 -0
  13. package/dist/channels/slack/index.js +122 -0
  14. package/dist/channels/telegram/adapter.d.ts +21 -0
  15. package/dist/channels/telegram/index.d.ts +2 -0
  16. package/dist/channels/telegram/index.js +179 -0
  17. package/dist/channels/telegram/types.d.ts +6 -0
  18. package/dist/channels/zalo/adapter.d.ts +22 -0
  19. package/dist/channels/zalo/index.d.ts +5 -0
  20. package/dist/channels/zalo/index.js +38337 -0
  21. package/dist/commands/modules/general.d.ts +4 -0
  22. package/dist/commands/modules/group.d.ts +4 -0
  23. package/dist/commands/modules/reaction.d.ts +4 -0
  24. package/dist/commands/router.d.ts +7 -0
  25. package/dist/commands/types.d.ts +15 -0
  26. package/dist/config/env.d.ts +13 -0
  27. package/dist/core/adapter.d.ts +12 -0
  28. package/dist/core/bus.d.ts +12 -0
  29. package/dist/core/context.d.ts +9 -0
  30. package/dist/core/hub.d.ts +14 -0
  31. package/dist/core/index.d.ts +5 -0
  32. package/dist/core/index.js +99 -0
  33. package/dist/core/types.d.ts +63 -0
  34. package/dist/index.cjs +39235 -0
  35. package/dist/index.d.ts +16 -0
  36. package/dist/index.js +39191 -0
  37. package/dist/login_personal.js +60 -0
  38. package/dist/oa/client.d.ts +25 -0
  39. package/dist/oa/index.d.ts +3 -0
  40. package/dist/oa/index.js +556 -0
  41. package/dist/personal/client.d.ts +162 -0
  42. package/dist/personal/index.d.ts +5 -0
  43. package/dist/personal/index.js +37098 -0
  44. package/package.json +107 -0
@@ -0,0 +1,122 @@
1
+ // src/core/adapter.ts
2
+ import { EventEmitter } from "node:events";
3
+
4
+ class BaseChannel extends EventEmitter {
5
+ _connected = false;
6
+ isConnected() {
7
+ return this._connected;
8
+ }
9
+ setConnected(value) {
10
+ const changed = this._connected !== value;
11
+ this._connected = value;
12
+ if (changed) {
13
+ this.emit("status", value ? "connected" : "disconnected");
14
+ }
15
+ }
16
+ }
17
+
18
+ // src/channels/slack/adapter.ts
19
+ class SlackChannelAdapter extends BaseChannel {
20
+ name = "slack";
21
+ config;
22
+ apiBase = "https://slack.com/api";
23
+ constructor(config) {
24
+ super();
25
+ this.config = config;
26
+ }
27
+ async connect() {
28
+ if (!this.config.botToken)
29
+ throw new Error("Slack botToken is required.");
30
+ await this.callApi("auth.test", {});
31
+ this.setConnected(true);
32
+ }
33
+ async disconnect() {
34
+ this.setConnected(false);
35
+ }
36
+ normalizeEvent(event) {
37
+ const msg = event.event || event;
38
+ if (!msg || msg.type !== "message")
39
+ return null;
40
+ if (msg.subtype === "bot_message" || msg.bot_id)
41
+ return null;
42
+ const isDm = msg.channel_type === "im" || msg.channel && msg.channel.startsWith("D");
43
+ return {
44
+ id: String(msg.client_msg_id || msg.ts),
45
+ channel: "slack",
46
+ sender: {
47
+ id: String(msg.user || ""),
48
+ isBot: Boolean(msg.bot_id)
49
+ },
50
+ chat: {
51
+ id: String(msg.channel),
52
+ type: isDm ? "dm" : "channel"
53
+ },
54
+ content: {
55
+ text: msg.text || "",
56
+ replyToId: msg.thread_ts ? String(msg.thread_ts) : undefined
57
+ },
58
+ raw: event,
59
+ timestamp: msg.ts ? parseFloat(msg.ts) * 1000 : Date.now()
60
+ };
61
+ }
62
+ async callApi(method, body) {
63
+ const res = await fetch(`${this.apiBase}/${method}`, {
64
+ method: "POST",
65
+ headers: {
66
+ Authorization: `Bearer ${this.config.botToken}`,
67
+ "Content-Type": "application/json"
68
+ },
69
+ body: JSON.stringify(body)
70
+ });
71
+ if (!res.ok) {
72
+ const errText = await res.text();
73
+ throw new Error(`Slack API ${method} failed: ${res.status} ${errText}`);
74
+ }
75
+ const data = await res.json();
76
+ if (!data.ok) {
77
+ throw new Error(`Slack API ${method} error: ${data.error}`);
78
+ }
79
+ return data;
80
+ }
81
+ async sendText(chatId, text, options) {
82
+ const payload = {
83
+ channel: chatId,
84
+ text
85
+ };
86
+ if (options?.replyToId) {
87
+ payload.thread_ts = options.replyToId;
88
+ }
89
+ const res = await this.callApi("chat.postMessage", payload);
90
+ return {
91
+ messageId: String(res.ts),
92
+ chatId,
93
+ timestamp: parseFloat(res.ts) * 1000
94
+ };
95
+ }
96
+ async sendMedia(chatId, media, options) {
97
+ const payload = {
98
+ channel: chatId,
99
+ text: media.caption || "Attachment"
100
+ };
101
+ if (options?.replyToId) {
102
+ payload.thread_ts = options.replyToId;
103
+ }
104
+ const res = await this.callApi("chat.postMessage", payload);
105
+ return {
106
+ messageId: String(res.ts),
107
+ chatId,
108
+ timestamp: parseFloat(res.ts) * 1000
109
+ };
110
+ }
111
+ async addReaction(chatId, messageId, emoji) {
112
+ const cleanName = emoji.replace(/:/g, "");
113
+ await this.callApi("reactions.add", {
114
+ channel: chatId,
115
+ timestamp: messageId,
116
+ name: cleanName
117
+ });
118
+ }
119
+ }
120
+ export {
121
+ SlackChannelAdapter
122
+ };
@@ -0,0 +1,21 @@
1
+ import { BaseChannel } from "../../core/adapter";
2
+ import type { ChannelType, MediaPayload, SendOptions, SentMessageResult, UnifiedMessage } from "../../core/types";
3
+ import type { TelegramAdapterConfig } from "./types";
4
+ export declare class TelegramChannelAdapter extends BaseChannel {
5
+ readonly name: ChannelType;
6
+ private config;
7
+ private apiRoot;
8
+ private pollTimer;
9
+ private lastUpdateId;
10
+ private isPolling;
11
+ constructor(config: TelegramAdapterConfig);
12
+ connect(): Promise<void>;
13
+ disconnect(): Promise<void>;
14
+ normalizeUpdate(update: any): UnifiedMessage | null;
15
+ private callApi;
16
+ private startPolling;
17
+ private stopPolling;
18
+ sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
19
+ sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
20
+ addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
21
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./adapter";
@@ -0,0 +1,179 @@
1
+ // src/core/adapter.ts
2
+ import { EventEmitter } from "node:events";
3
+
4
+ class BaseChannel extends EventEmitter {
5
+ _connected = false;
6
+ isConnected() {
7
+ return this._connected;
8
+ }
9
+ setConnected(value) {
10
+ const changed = this._connected !== value;
11
+ this._connected = value;
12
+ if (changed) {
13
+ this.emit("status", value ? "connected" : "disconnected");
14
+ }
15
+ }
16
+ }
17
+
18
+ // src/channels/telegram/adapter.ts
19
+ class TelegramChannelAdapter extends BaseChannel {
20
+ name = "telegram";
21
+ config;
22
+ apiRoot;
23
+ pollTimer = null;
24
+ lastUpdateId = 0;
25
+ isPolling = false;
26
+ constructor(config) {
27
+ super();
28
+ this.config = config;
29
+ this.apiRoot = config.apiRoot || "https://api.telegram.org";
30
+ }
31
+ async connect() {
32
+ if (!this.config.botToken) {
33
+ throw new Error("Telegram botToken is required.");
34
+ }
35
+ this.setConnected(true);
36
+ if (this.config.autoStart !== false) {
37
+ this.startPolling();
38
+ }
39
+ }
40
+ async disconnect() {
41
+ this.stopPolling();
42
+ this.setConnected(false);
43
+ }
44
+ normalizeUpdate(update) {
45
+ if (!update)
46
+ return null;
47
+ const msg = update.message || update.edited_message || update.channel_post;
48
+ if (!msg)
49
+ return null;
50
+ const chatType = msg.chat.type === "private" ? "dm" : msg.chat.type === "channel" ? "channel" : "group";
51
+ const senderName = [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ");
52
+ return {
53
+ id: String(msg.message_id),
54
+ channel: "telegram",
55
+ sender: {
56
+ id: String(msg.from?.id ?? ""),
57
+ name: senderName || undefined,
58
+ username: msg.from?.username,
59
+ isBot: Boolean(msg.from?.is_bot)
60
+ },
61
+ chat: {
62
+ id: String(msg.chat.id),
63
+ type: chatType,
64
+ title: msg.chat.title
65
+ },
66
+ content: {
67
+ text: msg.text || msg.caption || "",
68
+ replyToId: msg.reply_to_message?.message_id ? String(msg.reply_to_message.message_id) : undefined
69
+ },
70
+ raw: update,
71
+ timestamp: (msg.date || Math.floor(Date.now() / 1000)) * 1000
72
+ };
73
+ }
74
+ async callApi(method, body) {
75
+ const url = `${this.apiRoot}/bot${this.config.botToken}/${method}`;
76
+ const res = await fetch(url, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(body)
80
+ });
81
+ if (!res.ok) {
82
+ const errText = await res.text();
83
+ throw new Error(`Telegram API ${method} failed: ${res.status} ${errText}`);
84
+ }
85
+ const data = await res.json();
86
+ if (!data.ok) {
87
+ throw new Error(`Telegram API ${method} error: ${data.description}`);
88
+ }
89
+ return data.result;
90
+ }
91
+ startPolling() {
92
+ if (this.isPolling)
93
+ return;
94
+ this.isPolling = true;
95
+ const interval = this.config.pollIntervalMs || 1000;
96
+ const poll = async () => {
97
+ if (!this.isPolling)
98
+ return;
99
+ try {
100
+ const updates = await this.callApi("getUpdates", {
101
+ offset: this.lastUpdateId + 1,
102
+ timeout: 10
103
+ });
104
+ if (Array.isArray(updates)) {
105
+ for (const u of updates) {
106
+ this.lastUpdateId = Math.max(this.lastUpdateId, u.update_id);
107
+ const unified = this.normalizeUpdate(u);
108
+ if (unified) {
109
+ this.emit("message", unified);
110
+ }
111
+ }
112
+ }
113
+ } catch (err) {
114
+ this.emit("error", err);
115
+ } finally {
116
+ if (this.isPolling) {
117
+ this.pollTimer = setTimeout(poll, interval);
118
+ }
119
+ }
120
+ };
121
+ poll();
122
+ }
123
+ stopPolling() {
124
+ this.isPolling = false;
125
+ if (this.pollTimer) {
126
+ clearTimeout(this.pollTimer);
127
+ this.pollTimer = null;
128
+ }
129
+ }
130
+ async sendText(chatId, text, options) {
131
+ const payload = {
132
+ chat_id: chatId,
133
+ text
134
+ };
135
+ if (options?.replyToId) {
136
+ payload.reply_to_message_id = Number(options.replyToId);
137
+ }
138
+ const res = await this.callApi("sendMessage", payload);
139
+ return {
140
+ messageId: String(res.message_id),
141
+ chatId,
142
+ timestamp: res.date * 1000
143
+ };
144
+ }
145
+ async sendMedia(chatId, media, options) {
146
+ const method = media.type === "image" ? "sendPhoto" : media.type === "video" ? "sendVideo" : "sendDocument";
147
+ const payload = {
148
+ chat_id: chatId,
149
+ caption: media.caption
150
+ };
151
+ if (typeof media.source === "string") {
152
+ if (media.type === "image")
153
+ payload.photo = media.source;
154
+ else if (media.type === "video")
155
+ payload.video = media.source;
156
+ else
157
+ payload.document = media.source;
158
+ }
159
+ if (options?.replyToId) {
160
+ payload.reply_to_message_id = Number(options.replyToId);
161
+ }
162
+ const res = await this.callApi(method, payload);
163
+ return {
164
+ messageId: String(res.message_id),
165
+ chatId,
166
+ timestamp: (res.date || Date.now()) * 1000
167
+ };
168
+ }
169
+ async addReaction(chatId, messageId, emoji) {
170
+ await this.callApi("setMessageReaction", {
171
+ chat_id: chatId,
172
+ message_id: Number(messageId),
173
+ reaction: [{ type: "emoji", emoji }]
174
+ });
175
+ }
176
+ }
177
+ export {
178
+ TelegramChannelAdapter
179
+ };
@@ -0,0 +1,6 @@
1
+ export interface TelegramAdapterConfig {
2
+ botToken: string;
3
+ apiRoot?: string;
4
+ autoStart?: boolean;
5
+ pollIntervalMs?: number;
6
+ }
@@ -0,0 +1,22 @@
1
+ import { BaseChannel } from "../../core/adapter";
2
+ import type { ChannelType, MediaPayload, SendOptions, SentMessageResult } from "../../core/types";
3
+ export interface ZaloAdapterConfig {
4
+ api?: any;
5
+ credentialsPath?: string;
6
+ ownId?: string;
7
+ defaultIsGroup?: boolean;
8
+ }
9
+ export declare class ZaloChannelAdapter extends BaseChannel {
10
+ readonly name: ChannelType;
11
+ private api;
12
+ private ownId?;
13
+ private config;
14
+ constructor(config?: ZaloAdapterConfig);
15
+ connect(): Promise<void>;
16
+ disconnect(): Promise<void>;
17
+ private setupEventListener;
18
+ private normalizeMessage;
19
+ sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
20
+ sendMedia(chatId: string, media: MediaPayload, _options?: SendOptions): Promise<SentMessageResult>;
21
+ addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
22
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./adapter";
2
+ export { ZaloPersonalBot } from "../../personal/client";
3
+ export { ZaloOABot } from "../../oa/client";
4
+ export { initPersonalBot } from "../../personal/index";
5
+ export { initOABot } from "../../oa/index";