UNPKG

koishi-plugin-adapter-telegram-ex

Version:
731 lines (714 loc) 26.6 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { __markAsModule(target); for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __reExport = (target, module2, desc) => { if (module2 && typeof module2 === "object" || typeof module2 === "function") { for (let key of __getOwnPropNames(module2)) if (!__hasOwnProp.call(target, key) && key !== "default") __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable }); } return target; }; var __toModule = (module2) => { return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2); }; // plugins/adapter/telegram/src/index.ts __export(exports, { AdapterConfig: () => AdapterConfig, BotConfig: () => BotConfig, HttpPolling: () => HttpPolling, HttpServer: () => HttpServer, Sender: () => Sender, SenderError: () => SenderError, Telegram: () => types_exports, TelegramBot: () => TelegramBot, adaptGuildMember: () => adaptGuildMember, adaptUser: () => adaptUser, default: () => src_default }); var import_koishi6 = __toModule(require("koishi")); // plugins/adapter/telegram/src/bot.ts var import_koishi4 = __toModule(require("koishi")); // plugins/adapter/telegram/src/types/index.ts var types_exports = {}; __export(types_exports, { Internal: () => Internal }); // plugins/adapter/telegram/src/types/internal.ts var import_form_data = __toModule(require("form-data")); var import_koishi = __toModule(require("koishi")); var logger = new import_koishi.Logger("telegram"); var Internal = class { constructor(http) { this.http = http; } static define(method) { Internal.prototype[method] = async function(data = {}) { logger.debug("[request] %s %o", method, data); let response; if (data instanceof import_form_data.default) { response = await this.http.post("/" + method, data, { headers: data.getHeaders() }); } else { response = await this.http.post("/" + method, data); } logger.debug("[response] %o", response); const { ok, result } = response; if (ok) return result; throw new Error("Telegram API error: " + response); }; } }; __name(Internal, "Internal"); // plugins/adapter/telegram/src/types/inline.ts Internal.define("answerInlineQuery"); // plugins/adapter/telegram/src/types/game.ts Internal.define("sendGame"); Internal.define("setGameScore"); Internal.define("getGameHighScores"); // plugins/adapter/telegram/src/types/passport.ts Internal.define("setPassportDataErrors"); // plugins/adapter/telegram/src/types/payment.ts Internal.define("sendInvoice"); Internal.define("answerShippingQuery"); Internal.define("answerPreCheckoutQuery"); // plugins/adapter/telegram/src/types/sticker.ts Internal.define("sendSticker"); Internal.define("getStickerSet"); Internal.define("uploadStickerFile"); Internal.define("createNewStickerSet"); Internal.define("addStickerToSet"); Internal.define("setStickerPositionInSet"); Internal.define("deleteStickerFromSet"); Internal.define("setStickerSetThumb"); // plugins/adapter/telegram/src/types/update.ts Internal.define("getUpdates"); Internal.define("setWebhook"); Internal.define("deleteWebhook"); Internal.define("getWebhookInfo"); // plugins/adapter/telegram/src/types/index.ts Internal.define("getMe"); Internal.define("logOut"); Internal.define("close"); Internal.define("sendMessage"); Internal.define("forwardMessage"); Internal.define("copyMessage"); Internal.define("sendPhoto"); Internal.define("sendAudio"); Internal.define("sendDocument"); Internal.define("sendVideo"); Internal.define("sendAnimation"); Internal.define("sendVoice"); Internal.define("sendVideoNote"); Internal.define("sendMediaGroup"); Internal.define("sendLocation"); Internal.define("editMessageLiveLocation"); Internal.define("stopMessageLiveLocation"); Internal.define("sendVenue"); Internal.define("sendContact"); Internal.define("sendPoll"); Internal.define("sendDice"); Internal.define("sendChatAction"); Internal.define("getUserProfilePhotos"); Internal.define("getFile"); Internal.define("banChatMember"); Internal.define("unbanChatMember"); Internal.define("restrictChatMember"); Internal.define("promoteChatMember"); Internal.define("setChatAdministratorCustomTitle"); Internal.define("banChatSenderChat"); Internal.define("unbanChatSenderChat"); Internal.define("setChatPermissions"); Internal.define("exportChatInviteLink"); Internal.define("createChatInviteLink"); Internal.define("editChatInviteLink"); Internal.define("revokeChatInviteLink"); Internal.define("approveChatJoinRequest"); Internal.define("declineChatJoinRequest"); Internal.define("setChatPhoto"); Internal.define("deleteChatPhoto"); Internal.define("setChatTitle"); Internal.define("setChatDescription"); Internal.define("pinChatMessage"); Internal.define("unpinChatMessage"); Internal.define("unpinAllChatMessages"); Internal.define("leaveChat"); Internal.define("getChat"); Internal.define("getChatAdministrators"); Internal.define("getChatMemberCount"); Internal.define("getChatMember"); Internal.define("setChatStickerSet"); Internal.define("deleteChatStickerSet"); Internal.define("answerCallbackQuery"); Internal.define("setMyCommands"); Internal.define("deleteMyCommands"); Internal.define("getMyCommands"); Internal.define("editMessageText"); Internal.define("editMessageCaption"); Internal.define("editMessageMedia"); Internal.define("editMessageReplyMarkup"); Internal.define("stopPoll"); Internal.define("deleteMessage"); // plugins/adapter/telegram/src/utils.ts var import_koishi2 = __toModule(require("koishi")); var AdapterConfig = import_koishi2.Schema.object({ path: import_koishi2.Schema.string().description("服务器监听的路径。").default("/telegram"), selfUrl: import_koishi2.Schema.string().role("url").description("Koishi 服务暴露在公网的地址。缺省时将使用全局配置。") }); var adaptUser = /* @__PURE__ */ __name((data) => ({ userId: data.id.toString(), username: data.username, nickname: data.first_name + (data.last_name || ""), isBot: data.is_bot }), "adaptUser"); var adaptGuildMember = /* @__PURE__ */ __name((data) => adaptUser(data.user), "adaptGuildMember"); // plugins/adapter/telegram/src/sender.ts var import_fs = __toModule(require("fs")); var import_koishi3 = __toModule(require("koishi")); var import_es_aggregate_error = __toModule(require("es-aggregate-error")); var import_file_type = __toModule(require("file-type")); var import_form_data2 = __toModule(require("form-data")); var logger2 = new import_koishi3.Logger("telegram"); var prefixTypes = ["quote", "card", "anonymous", "markdown"]; async function maybeFile(payload, field) { if (!payload[field]) return []; let content; let filename = "file"; const [schema, data] = payload[field].split("://"); if (schema === "file") { content = (0, import_fs.createReadStream)(data); delete payload[field]; } else if (schema === "base64") { content = Buffer.from(data, "base64"); delete payload[field]; } if (field === "document" && schema === "base64") { const type = await import_file_type.default.fromBuffer(Buffer.from(data, "base64")); if (!type) { logger2.warn("Can not infer file mime"); } else filename = `file.${type.ext}`; } return [field, content, filename]; } __name(maybeFile, "maybeFile"); async function isGif(url) { if (url.toLowerCase().endsWith(".gif")) return true; const [schema, data] = url.split("://"); if (schema === "base64") { const type = await import_file_type.default.fromBuffer(Buffer.from(data, "base64")); if (!type) { logger2.warn("Can not infer file mime"); } else if (type.ext === "gif") return true; } return false; } __name(isGif, "isGif"); var assetApi = { photo: "sendPhoto", audio: "sendAudio", document: "sendDocument", video: "sendVideo", animation: "sendAnimation" }; var Sender = class { constructor(bot, chat_id) { this.bot = bot; this.chat_id = chat_id; this.errors = []; this.results = []; this.currAssetType = null; this.sendAsset = async () => { const [field, content, filename] = await maybeFile(this.payload, this.currAssetType); const payload = new import_form_data2.default(); for (const key in this.payload) { payload.append(key, this.payload[key].toString()); } if (field && content) payload.append(field, content, filename); this.results.push(await this.bot.internal[assetApi[this.currAssetType]](payload)); this.currAssetType = null; delete this.payload[this.currAssetType]; delete this.payload.reply_to_message; this.payload.caption = ""; }; this.payload = { chat_id, caption: "" }; } static from(bot, chat_id) { const sender = new Sender(bot, chat_id); return sender.sendMessage.bind(sender); } async sendMessage(content) { const segs = import_koishi3.segment.parse(content); let currIdx = 0; while (currIdx < segs.length && prefixTypes.includes(segs[currIdx].type)) { if (segs[currIdx].type === "quote") { this.payload.reply_to_message_id = segs[currIdx].data.id; } else if (segs[currIdx].type === "anonymous") { if (segs[currIdx].data.ignore === "false") return null; } else if (segs[currIdx].type === "markdown") { this.payload.parse_mode = "MarkdownV2"; } ++currIdx; } for (const seg of segs.slice(currIdx)) { switch (seg.type) { case "text": this.payload.caption += seg.data.content; break; case "at": { const atTarget = seg.data.name || seg.data.id || seg.data.role || seg.data.type; if (!atTarget) break; this.payload.caption += `@${atTarget} `; break; } case "sharp": { const sharpTarget = seg.data.name || seg.data.id; if (!sharpTarget) break; this.payload.caption += `#${sharpTarget} `; break; } case "face": logger2.warn("Telegram don't support face"); break; case "image": case "audio": case "video": case "file": { if (this.currAssetType) await this.sendAsset(); const assetUrl = seg.data.url; if (!assetUrl) { logger2.warn("asset segment with no url"); break; } if (seg.type === "image") this.currAssetType = await isGif(assetUrl) ? "animation" : "photo"; else if (seg.type === "file") this.currAssetType = "document"; else this.currAssetType = seg.type; this.payload[this.currAssetType] = assetUrl; break; } default: logger2.warn(`Unexpected asset type: ${seg.type}`); return; } } if (this.currAssetType) await this.sendAsset(); if (this.payload.caption) { this.results.push(await this.bot.internal.sendMessage({ chat_id: this.chat_id, text: this.payload.caption, reply_to_message_id: this.payload.reply_to_message_id })); } if (!this.errors.length) return this.results.map((result) => "" + result.message_id); throw new import_es_aggregate_error.default(this.errors); } }; __name(Sender, "Sender"); // plugins/adapter/telegram/src/bot.ts var import_fs2 = __toModule(require("fs")); var logger3 = new import_koishi4.Logger("telegram"); var SenderError = class extends Error { constructor(args, url, retcode, selfId) { super(`Error when trying to send to ${url}, args: ${JSON.stringify(args)}, retcode: ${retcode}`); Object.defineProperties(this, { name: { value: "SenderError" }, selfId: { value: selfId }, code: { value: retcode }, args: { value: args }, url: { value: url } }); } }; __name(SenderError, "SenderError"); var BotConfig = import_koishi4.Schema.intersect([ import_koishi4.Schema.object({ token: import_koishi4.Schema.string().description("机器人的用户令牌。").role("secret").required(), files: import_koishi4.Schema.object({ endpoint: import_koishi4.Schema.string().description("文件请求的终结点。"), local: import_koishi4.Schema.boolean().description("是否启用 [Telegram Bot API](https://github.com/tdlib/telegram-bot-api) 本地模式。").default(false) }) }), import_koishi4.Quester.createSchema({ endpoint: "https://api.telegram.org" }) ]); var _TelegramBot = class extends import_koishi4.Bot { constructor(adapter, config) { (0, import_koishi4.assertProperty)(config, "token"); super(adapter, config); this.selfId = config.token.split(":")[0]; this.local = config.files.local; this.http = this.app.http.extend(__spreadProps(__spreadValues({}, config), { endpoint: `${config.endpoint}/bot${config.token}` })); this.http.file = this.app.http.extend(__spreadProps(__spreadValues({}, config), { endpoint: `${config.files.endpoint || config.endpoint}/file/bot${config.token}` })); this.internal = new Internal(this.http); } async sendMessage(channelId, content) { if (!content) return []; let subtype; let chatId; if (channelId.startsWith("private:")) { subtype = "private"; chatId = channelId.slice(8); } else { subtype = "group"; chatId = channelId; } const session = await this.session({ subtype, content, channelId, guildId: channelId }); if (!(session == null ? void 0 : session.content)) return []; const send = Sender.from(this, chatId); const results = await send(session.content); for (const id of results) { session.messageId = id; this.app.emit(session, "send", session); } return results; } async sendPrivateMessage(userId, content) { return this.sendMessage("private:" + userId, content); } async getMessage() { return null; } async deleteMessage(chat_id, message_id) { message_id = +message_id; await this.internal.deleteMessage({ chat_id, message_id }); } static adaptGroup(data) { (0, import_koishi4.renameProperty)(data, "guildId", "id"); (0, import_koishi4.renameProperty)(data, "guildName", "title"); return data; } async getGuild(chat_id) { const data = await this.internal.getChat({ chat_id }); return _TelegramBot.adaptGroup(data); } async getGuildList() { return []; } async getGuildMember(chat_id, user_id) { user_id = +user_id; if (Number.isNaN(user_id)) return null; const data = await this.internal.getChatMember({ chat_id, user_id }); return adaptGuildMember(data); } async getGuildMemberList(chat_id) { const data = await this.internal.getChatAdministrators({ chat_id }); return data.map(adaptGuildMember); } setGroupLeave(chat_id) { return this.internal.leaveChat({ chat_id }); } async handleGuildMemberRequest(messageId, approve, comment) { const [chat_id, user_id] = messageId.split("@"); const method = approve ? "approveChatJoinRequest" : "declineChatJoinRequest"; const success = await this.internal[method]({ chat_id, user_id: +user_id }); if (!success) throw new Error(`handel guild member request field ${success}`); } async getLoginInfo() { const data = await this.internal.getMe(); return adaptUser(data); } async $getFileData(file_id) { try { const file = await this.internal.getFile({ file_id }); return await this.$getFileContent(file.file_path); } catch (e) { logger3.warn("get file error", e); } } async $getFileContent(filePath) { let res; if (this.local) { res = await import_fs2.default.promises.readFile(filePath); } else { res = await this.http.file.get(`/${filePath}`, { responseType: "arraybuffer" }); } const base64 = `base64://` + res.toString("base64"); return { url: base64 }; } }; var TelegramBot = _TelegramBot; __name(TelegramBot, "TelegramBot"); TelegramBot.schema = AdapterConfig; // plugins/adapter/telegram/src/http.ts var import_koishi5 = __toModule(require("koishi")); var logger4 = new import_koishi5.Logger("telegram"); var TelegramAdapter = class extends import_koishi5.Adapter { async connect(bot) { const { username, userId, avatar, nickname } = await bot.getLoginInfo(); bot.username = username; bot.avatar = avatar; bot.selfId = userId; bot.nickname = nickname; await this.listenUpdates(bot); logger4.debug("connected to %c", "telegram:" + bot.selfId); bot.resolve(); } async onUpdate(update, bot) { logger4.debug("receive %s", JSON.stringify(update)); const session = { selfId: bot.selfId }; session.telegram = Object.create(bot.internal); Object.assign(session.telegram, update); function parseText(text, entities) { let curr = 0; const segs = []; for (const e of entities) { const eText = text.substr(e.offset, e.length); let handleCurrent = true; if (e.type === "mention") { if (eText[0] !== "@") throw new Error("Telegram mention does not start with @: " + eText); const atName = eText.slice(1); if (eText === "@" + bot.username) segs.push({ type: "at", data: { id: bot.selfId, name: atName } }); } else if (e.type === "text_mention") { segs.push({ type: "at", data: { id: e.user.id } }); } else { handleCurrent = false; } if (handleCurrent && e.offset > curr) { segs.push({ type: "text", data: { content: text.slice(curr, e.offset) } }); curr = e.offset + e.length; } } if (curr < (text == null ? void 0 : text.length) || 0) { segs.push({ type: "text", data: { content: text.slice(curr) } }); } return segs; } __name(parseText, "parseText"); const message = update.message || update.edited_message || update.channel_post || update.edited_channel_post; if (message) { session.messageId = message.message_id.toString(); session.type = update.message || update.channel_post ? "message" : "message-updated"; session.timestamp = message.date * 1e3; const segments = []; if (message.reply_to_message) { const replayText = message.reply_to_message.text || message.reply_to_message.caption; const parsedReply = parseText(replayText, message.reply_to_message.entities || []); session.quote = { messageId: message.reply_to_message.message_id.toString(), author: adaptUser(message.reply_to_message.from), content: replayText ? import_koishi5.segment.join(parsedReply) : void 0 }; segments.push({ type: "quote", data: { id: message.reply_to_message.message_id, channelId: message.reply_to_message.chat.id } }); } if (message.location) { segments.push({ type: "location", data: { lat: message.location.latitude, lon: message.location.longitude } }); } if (message.photo) { const photo = message.photo.sort((s1, s2) => s2.file_size - s1.file_size)[0]; segments.push({ type: "image", data: await bot.$getFileData(photo.file_id) }); } if (message.sticker) { try { const file = await bot.internal.getFile({ file_id: message.sticker.file_id }); if (file.file_path.endsWith(".tgs")) { throw new Error("tgs is not supported now"); } segments.push({ type: "image", data: await bot.$getFileContent(file.file_path) }); } catch (e) { logger4.warn("get file error", e); segments.push({ type: "text", data: { content: `[${message.sticker.set_name || "sticker"} ${message.sticker.emoji || ""}]` } }); } } else if (message.animation) { segments.push({ type: "image", data: await bot.$getFileData(message.animation.file_id) }); } else if (message.voice) { segments.push({ type: "audio", data: await bot.$getFileData(message.voice.file_id) }); } else if (message.video) { segments.push({ type: "video", data: await bot.$getFileData(message.video.file_id) }); } else if (message.document) { segments.push({ type: "file", data: await bot.$getFileData(message.document.file_id) }); } const msgText = message.text || message.caption; segments.push(...parseText(msgText, message.entities || [])); session.content = import_koishi5.segment.join(segments); session.userId = message.from.id.toString(); session.author = adaptUser(message.from); session.channelId = message.chat.id.toString(); if (message.chat.type === "private") { session.subtype = "private"; session.channelId = "private:" + session.channelId; } else { session.subtype = "group"; session.guildId = session.channelId; } } else if (update.chat_join_request) { session.timestamp = update.chat_join_request.date * 1e3; session.type = "guild-member-request"; session.messageId = `${update.chat_join_request.chat.id}@${update.chat_join_request.from.id}`; session.content = ""; session.channelId = update.chat_join_request.chat.id.toString(); session.guildId = session.channelId; } logger4.debug("receive %o", session); this.dispatch(new import_koishi5.Session(bot, session)); } }; __name(TelegramAdapter, "TelegramAdapter"); var HttpServer = class extends TelegramAdapter { constructor(ctx, config) { super(ctx, config); config.path = (0, import_koishi5.sanitize)(config.path || "/telegram"); if (config.selfUrl) { config.selfUrl = (0, import_koishi5.trimSlash)(config.selfUrl); } else { config.selfUrl = (0, import_koishi5.assertProperty)(ctx.app.options, "selfUrl"); } } async listenUpdates(bot) { const { token } = bot.config; const { path, selfUrl } = this.config; const info = await bot.internal.setWebhook({ url: selfUrl + path + "?token=" + token, drop_pending_updates: true }); if (!info) throw new Error("Set webhook failed"); logger4.debug("listening updates %c", "telegram: " + bot.selfId); } async start() { const { path } = this.config; this.ctx.router.post(path, async (ctx) => { var _a; const payload = ctx.request.body; const token = ctx.request.query.token; const [selfId] = token.split(":"); const bot = this.bots.find((bot2) => bot2.selfId === selfId); if (!(((_a = bot == null ? void 0 : bot.config) == null ? void 0 : _a.token) === token)) return ctx.status = 403; ctx.body = "OK"; await this.onUpdate(payload, bot); }); } stop() { logger4.debug("http server closing"); } }; __name(HttpServer, "HttpServer"); HttpServer.schema = BotConfig; var HttpPolling = class extends TelegramAdapter { constructor() { super(...arguments); this.offset = {}; } start() { this.isStopped = false; } stop() { this.isStopped = true; } async listenUpdates(bot) { const { selfId } = bot; this.offset[selfId] = this.offset[selfId] || 0; const { url } = await bot.internal.getWebhookInfo(); if (url) { logger4.warn("Bot currently has a webhook set up, trying to remove it..."); await bot.internal.setWebhook({ url: "" }); } const previousUpdates = await bot.internal.getUpdates({ allowed_updates: [], timeout: 0 }); previousUpdates.forEach((e) => this.offset[selfId] = Math.max(this.offset[selfId], e.update_id)); const polling = /* @__PURE__ */ __name(async () => { const updates = await bot.internal.getUpdates({ offset: this.offset[selfId] + 1, timeout: bot.config.pollingTimeout }); for (const e of updates) { this.offset[selfId] = Math.max(this.offset[selfId], e.update_id); this.onUpdate(e, bot); } if (!this.isStopped) { setTimeout(polling, 0); } }, "polling"); polling(); logger4.debug("listening updates %c", "telegram: " + bot.selfId); } }; __name(HttpPolling, "HttpPolling"); HttpPolling.schema = import_koishi5.Schema.intersect([ BotConfig, import_koishi5.Schema.object({ pollingTimeout: import_koishi5.Schema.union([ import_koishi5.Schema.natural(), import_koishi5.Schema.transform(import_koishi5.Schema.const(true), () => 60) ]).default(60).description("通过长轮询获取更新时请求的超时 (单位为秒)。") }) ]); // plugins/adapter/telegram/src/index.ts var src_default = import_koishi6.Adapter.define("telegram", TelegramBot, { webhook: HttpServer, polling: HttpPolling }, ({ pollingTimeout }) => { return pollingTimeout ? "polling" : "webhook"; }); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AdapterConfig, BotConfig, HttpPolling, HttpServer, Sender, SenderError, Telegram, TelegramBot, adaptGuildMember, adaptUser }); //# sourceMappingURL=index.js.map