koishi-plugin-adapter-telegram-ex
Version:
Telegram Adapter for Koishi
731 lines (714 loc) • 26.6 kB
JavaScript
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