diff --git a/bun.lock b/bun.lock index 4eb00d7..4d37b82 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,8 @@ "@openrouter/sdk": "^0.12.79", "chalk": "^5.6.2", "commander": "^15.0.0", + "discord.js": "^14.26.4", + "gramio": "^0.12.0", "prettier": "^3.8.3", "supermemory": "^4.24.12", "yaml": "^2.9.0", @@ -22,24 +24,94 @@ }, }, "packages": { + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + + "@gramio/callback-data": ["@gramio/callback-data@0.1.0", "", {}, "sha512-eqpR2Bod7dySDLDu8RB3xJdcrwyE3ZrhHaP+v0xOqWbgOCGY+gZp37E2E0KeXq/ko/8DLp3XeTaQD61++kiK6A=="], + + "@gramio/composer": ["@gramio/composer@0.5.0", "", {}, "sha512-CP8DxNzDmVl2uO/L2VtjYycLi6jrPkZvTJoN/qVtct2Ms0oUKlKFgnNidH/i2R41HUow2fU0tlcGb1LR4NPHtg=="], + + "@gramio/contexts": ["@gramio/contexts@0.9.0", "", { "peerDependencies": { "@gramio/types": "^10.1.0", "inspectable": "^3.0.1" } }, "sha512-94TQN69DyREAE9TD0ayOXHRxouOlWr7JP0Fv1oFbLwyCgKBTK7+I3JAn2RZiGWDeQ8n0oTifoEqBDPLDrMgSzg=="], + + "@gramio/files": ["@gramio/files@0.6.1", "", { "dependencies": { "@gramio/types": "^10.0.0" } }, "sha512-by1v+a3D/9e5kO0Yl7Ob92iSXMdorqLvTscb+wwA4z6YBMGmnwL8I7CqzL0yFUUI0Xzi9S7uKbPBWm7VH4GZ8w=="], + + "@gramio/format": ["@gramio/format@0.9.0", "", { "dependencies": { "@gramio/types": "^10.1.0" }, "peerDependencies": { "marked": "^15.0.11", "node-html-parser": ">=6.0.0" }, "optionalPeers": ["marked", "node-html-parser"] }, "sha512-dU0DWlUo01wL+yL8Pi/itFfKhxvqn8WXKQ+maYsjTFSgfo3iHF96kAsrmdwszJjYSBioWcWD2DhKJB4S1k9dXA=="], + + "@gramio/keyboards": ["@gramio/keyboards@1.4.0", "", { "dependencies": { "@gramio/types": ">=9.6.0" } }, "sha512-sUjXV/LQmXIXRd2H0dwVXObeM+3OeUuxR/rt1hi2CUoyg5aaFy8wUfMjVMhx/9IXqHCQ7FK72zS9sqVXkvYCeg=="], + + "@gramio/types": ["@gramio/types@10.1.0", "", {}, "sha512-vced/i5k2wbkkcOZvD3LJgv7EjPK6oJOL12u0Piag+JDDSXJt/fejPPXXG+st0vYoIQR5bd4ndrWi4Cn5PshFQ=="], + "@openrouter/sdk": ["@openrouter/sdk@0.12.79", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-0ZpwtnuHh3/B1piW9kHCUIQy6PAsaK/vjFdZuHxmCdAenCyUNsLA2mFpmfHNWRNb+bOO3yBc4IALa264UyzmBA=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="], + + "discord.js": ["discord.js@14.26.4", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "gramio": ["gramio@0.12.0", "", { "dependencies": { "@gramio/callback-data": "^0.1.0", "@gramio/composer": "^0.5.0", "@gramio/contexts": "^0.9.0", "@gramio/files": "^0.6.1", "@gramio/format": "^0.9.0", "@gramio/keyboards": "^1.4.0", "@gramio/types": "^10.1.0", "debug": "^4.4.3" } }, "sha512-SrUGgNbur5jqe8xf5e4a1QYEX5bbIPnJV+rclcBReRwGZwUShIR0aAahe3dRRqxYIXbxFr1oShYL7q/6Nwi+BQ=="], + + "inspectable": ["inspectable@3.0.2", "", {}, "sha512-XHygPjNXXe1TWQLlVdCYLejUklsGGo+XWS3Cn/RlkvnhkbZqcQxoXPrSh7Via5aATYJUMWVnrD5CbW2c+BtKMA=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], "supermemory": ["supermemory@4.24.12", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-xAFextuqk4JuoW33jJaFGqT1oMppN2IgfWUrV18Fv3qAAZ6M1SR1tb+7EBq8vrEQIx4iY2MQh5p+qnfL6lI8Yw=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], } } diff --git a/package.json b/package.json index 817db30..b64cd5e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@openrouter/sdk": "^0.12.79", "chalk": "^5.6.2", "commander": "^15.0.0", + "discord.js": "^14.26.4", + "gramio": "^0.12.0", "prettier": "^3.8.3", "supermemory": "^4.24.12", "yaml": "^2.9.0", diff --git a/src/brain/manager.ts b/src/brain/manager.ts index dd16ed8..b70b348 100644 --- a/src/brain/manager.ts +++ b/src/brain/manager.ts @@ -1,95 +1,99 @@ import { config } from "@/config"; -import { mkdir, readFile, rm, writeFile } from "fs/promises"; +import { mkdir, readFile, writeFile } from "fs/promises"; import { join } from "path"; +export type ChannelKeys = "discord" | "telegram"; +export interface BrainDiscordConfig { + token: string; +} +export interface BrainTelegramConfig { + token: string; +} export interface BrainItem { brainId: string; spaceName: string; displayName: string; baseSystemPrompt: string; activated: boolean; + channel?: ChannelKeys; + discord?: BrainDiscordConfig; + telegram?: BrainTelegramConfig; } +export type BrainItemDiscord = Omit & { + channel: "discord"; + discord: BrainDiscordConfig; +}; +export type BrainItemTelegram = Omit & { + channel: "telegram"; + discord: BrainTelegramConfig; +}; +export type BrainItemWithChannel = BrainItemDiscord | BrainItemTelegram; export type BrainList = BrainItem[]; -// Layout: -// /brains.json — BrainItem[] index, mirror -// //brain.json — BrainItem per brain, source of truth - export class BrainDBManager { constructor(private readonly root: string = config.brainboxRoot) {} - private brainDir(brainId: string): string { - return join(this.root, brainId); - } - - private brainFile(brainId: string): string { - return join(this.brainDir(brainId), "brain.json"); - } - - private indexFile(): string { + private dbFile(): string { return join(this.root, "brains.json"); } - private async readIndex(): Promise { + private async readDB(): Promise { try { - const content = await readFile(this.indexFile(), { encoding: "utf-8" }); + const content = await readFile(this.dbFile(), { encoding: "utf-8" }); return JSON.parse(content) as BrainList; } catch { return []; } } - private async writeIndex(list: BrainList): Promise { + private async writeDB(list: BrainList): Promise { await mkdir(this.root, { recursive: true }); - await writeFile(this.indexFile(), JSON.stringify(list, null, 2), { + await writeFile(this.dbFile(), JSON.stringify(list, null, 2), { encoding: "utf-8", }); } - private async writeBrain(brain: BrainItem): Promise { - await mkdir(this.brainDir(brain.brainId), { recursive: true }); - await writeFile( - this.brainFile(brain.brainId), - JSON.stringify(brain, null, 2), - { encoding: "utf-8" }, - ); - } - async loadBrain(brainId: string): Promise { - try { - const content = await readFile(this.brainFile(brainId), { - encoding: "utf-8", - }); - return JSON.parse(content) as BrainItem; - } catch { - return undefined; - } + const list = await this.readDB(); + return list.find((b) => b.brainId === brainId); } async saveBrain(brainId: string, brain: BrainItem): Promise { - await this.writeBrain(brain); - const list = await this.readIndex(); + const list = await this.readDB(); const idx = list.findIndex((b) => b.brainId === brainId); if (idx >= 0) list[idx] = brain; else list.push(brain); - await this.writeIndex(list); + await this.writeDB(list); } - async listBrain(): Promise> { - const list = await this.readIndex(); - return list.map(({ brainId, displayName }) => ({ brainId, displayName })); + async listAvailableBrain(): Promise { + return (await this.readDB()).filter((b) => + this.isBrainReady(b), + ) as BrainItemWithChannel[]; } async deleteBrain(brainId: string): Promise { - await rm(this.brainDir(brainId), { recursive: true, force: true }); - const list = await this.readIndex(); + const list = await this.readDB(); const filtered = list.filter((b) => b.brainId !== brainId); if (filtered.length === list.length) return; - await this.writeIndex(filtered); + await this.writeDB(filtered); } async isBrainAvailable(brainId: string): Promise { - return (await this.loadBrain(brainId)) !== undefined; + const item = await this.loadBrain(brainId); + return item !== undefined && this.isBrainReady(item); + } + + private isBrainReady(item: BrainItem): boolean { + if (!item.activated) return false; + switch (item.channel) { + case "discord": + return !!item.discord?.token; + case "telegram": + return !!item.telegram?.token; + default: + return false; + } } } diff --git a/src/channel/base.ts b/src/channel/base.ts index 2a6036a..f0726dd 100644 --- a/src/channel/base.ts +++ b/src/channel/base.ts @@ -1,11 +1,12 @@ import type { Brain } from "@/brain"; import type { AvailabilityStatus } from "@/openrouter/schema"; -export interface BaseChannel { - readonly brain: Brain; +export abstract class BaseChannel { + constructor(public readonly brain: Brain) {} - init(): Promise; + abstract init(): Promise; - send(text: string, opts?: { replyTo?: string }): Promise; - setAvailability(status: AvailabilityStatus): Promise; -} + abstract send(text: string, opts?: { replyTo?: string }): Promise; + + abstract setAvailability(status: AvailabilityStatus): Promise; +} \ No newline at end of file diff --git a/src/channel/discord.ts b/src/channel/discord.ts new file mode 100644 index 0000000..0a0d44a --- /dev/null +++ b/src/channel/discord.ts @@ -0,0 +1,34 @@ +import { Client, Events, GatewayIntentBits } from "discord.js"; +import { z } from "zod"; +import type { AvailabilityStatus } from "@/openrouter/schema"; +import { logger } from "@/utils/logger"; +import { BaseChannel } from "./base"; + +const discordConfigSchema = z.object({ + token: z.string().min(1), +}); + +export class DiscordChannel extends BaseChannel { + private client?: Client; + + async init(): Promise { + const { token } = discordConfigSchema.parse({ + token: this.brain.brainbase.discord?.token, + }); + this.client = new Client({ intents: [GatewayIntentBits.Guilds] }); + this.client.once(Events.ClientReady, (c) => { + logger.success(`Discord ready as ${c.user.tag}`); + }); + await this.client.login(token); + } + + async send(_text: string, _opts?: { replyTo?: string }): Promise { + // ponytail: stub — wire up this.client.channels to send + throw new Error("DiscordChannel.send not implemented"); + } + + async setAvailability(_status: AvailabilityStatus): Promise { + // ponytail: stub — wire up this.client.user.setPresence / setStatus + throw new Error("DiscordChannel.setAvailability not implemented"); + } +} diff --git a/src/channel/telegram.ts b/src/channel/telegram.ts new file mode 100644 index 0000000..12bc4dd --- /dev/null +++ b/src/channel/telegram.ts @@ -0,0 +1,34 @@ +import { Bot } from "gramio"; +import { z } from "zod"; +import type { AvailabilityStatus } from "@/openrouter/schema"; +import { logger } from "@/utils/logger"; +import { BaseChannel } from "./base"; + +const telegramConfigSchema = z.object({ + token: z.string().min(1), +}); + +export class TelegramChannel extends BaseChannel { + private bot?: Bot; + + async init(): Promise { + const { token } = telegramConfigSchema.parse({ + token: this.brain.brainbase.telegram?.token, + }); + this.bot = new Bot(token); + this.bot.onStart(({ info }) => { + logger.success(`Telegram ready as @${info.username}`); + }); + await this.bot.start(); + } + + async send(_text: string, _opts?: { replyTo?: string }): Promise { + // ponytail: stub — wire up this.bot.api.sendMessage + throw new Error("TelegramChannel.send not implemented"); + } + + async setAvailability(_status: AvailabilityStatus): Promise { + // ponytail: stub — Telegram has no presence concept; map to custom status or no-op + throw new Error("TelegramChannel.setAvailability not implemented"); + } +}