mirror of
https://github.com/uditkarode/bing-chat-telegram.git
synced 2025-07-26 08:40:35 +00:00
init
This commit is contained in:
72
src/ai.ts
Normal file
72
src/ai.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { bingChat } from "./instances.js";
|
||||
import { done, firstPos, queue } from "./queue.js";
|
||||
import { transformBingResponse } from "./transformers.js";
|
||||
import { ChatMessage } from "bing-chat";
|
||||
import { Context } from "telegraf";
|
||||
import { bold, fmt } from "telegraf/format";
|
||||
import { Update } from "typegram";
|
||||
|
||||
const chats: Record<
|
||||
number,
|
||||
{ index: number; res?: ChatMessage; variant?: string }
|
||||
> = {};
|
||||
|
||||
export const variants = ["creative", "balanced", "precise"];
|
||||
const defaultVariant = "Balanced";
|
||||
|
||||
export async function ai(ctx: Context<Update>, prompt: string) {
|
||||
try {
|
||||
const chatId = ctx.chat!.id;
|
||||
chats[chatId] ||= { index: 1 };
|
||||
|
||||
const { pos, turn } = queue();
|
||||
if (pos > firstPos) {
|
||||
await ctx.reply(fmt`Queued at position: ${bold`${pos}`}`);
|
||||
await turn;
|
||||
}
|
||||
|
||||
const { message_id } = await ctx.reply(
|
||||
fmt`${bold`[${chats[chatId].index++}]`} Running prompt...`,
|
||||
);
|
||||
|
||||
const bingRes = await bingChat.sendMessage(
|
||||
prompt,
|
||||
Object.assign({}, chats[chatId].res, {
|
||||
variant: chats[chatId].variant ?? defaultVariant,
|
||||
}),
|
||||
);
|
||||
chats[chatId].res = bingRes;
|
||||
|
||||
let tgRes = transformBingResponse(bingRes);
|
||||
|
||||
// Bing Chat often replies with the exact prompt
|
||||
// in case it's unable to continue the conversation.
|
||||
if (tgRes.text === prompt && !tgRes.entities) {
|
||||
tgRes = fmt`Something went wrong. Starting a new chat with /newchat is recommended.`;
|
||||
}
|
||||
|
||||
await ctx.telegram.editMessageText(chatId, message_id, undefined, tgRes, {
|
||||
disable_web_page_preview: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await ctx.reply("Something went wrong!");
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
export function newChat(chatId: number) {
|
||||
delete chats[chatId].res;
|
||||
chats[chatId].index = 1;
|
||||
}
|
||||
|
||||
export function getVariant(chatId: number) {
|
||||
return chats[chatId]?.variant ?? defaultVariant;
|
||||
}
|
||||
|
||||
export function setVariant(chatId: number, variant: string) {
|
||||
chats[chatId] ||= { index: 1 };
|
||||
variant = variant.toLowerCase();
|
||||
chats[chatId].variant = variant.charAt(0).toUpperCase() + variant.slice(1);
|
||||
}
|
9
src/check-origin.ts
Normal file
9
src/check-origin.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Context, Middleware } from "telegraf";
|
||||
|
||||
export const checkOrigin =
|
||||
(allowedIds: readonly number[] | number[]): Middleware<Context> =>
|
||||
(ctx, next) => {
|
||||
const chatId = ctx.chat?.id;
|
||||
if (chatId && allowedIds.includes(chatId)) next();
|
||||
else ctx.reply("Unable to proceed in this chat!");
|
||||
};
|
129
src/fmt-replace.ts
Normal file
129
src/fmt-replace.ts
Normal file
@ -0,0 +1,129 @@
|
||||
// This code is licensed under the MIT License
|
||||
// Copyright (c) MKRhere (https://mkr.pw)
|
||||
import { FmtString } from "telegraf/format";
|
||||
import { MessageEntity } from "typegram";
|
||||
|
||||
interface EntityCompare {
|
||||
offset: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/** get the starting of the entity */
|
||||
const starts = (e: EntityCompare) => e.offset;
|
||||
|
||||
/** get the ending of the entity */
|
||||
const ends = (e: EntityCompare) => e.offset + e.length;
|
||||
|
||||
const before = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B ends before A starts
|
||||
ends(B) <= starts(A);
|
||||
|
||||
const after = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B starts after A ends
|
||||
starts(B) >= ends(A);
|
||||
|
||||
const inside = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B starts with/after A and ends before A
|
||||
(starts(B) >= starts(A) && ends(B) < ends(A)) ||
|
||||
// B starts after A and ends before/with A
|
||||
(starts(B) > starts(A) && ends(B) <= ends(A));
|
||||
|
||||
const contains = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B starts before/with A and ends with/after A
|
||||
starts(B) <= starts(A) && ends(B) >= ends(A);
|
||||
|
||||
const endsInside = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B starts before A starts, ends after A starts, ends before B ends
|
||||
starts(B) < starts(A) && ends(B) > starts(A) && ends(B) < ends(A);
|
||||
|
||||
const startsInside = (A: EntityCompare, B: EntityCompare) =>
|
||||
// B starts after A, starts before A ends, ends after A
|
||||
starts(B) > starts(A) && starts(B) < ends(A) && ends(B) > ends(A);
|
||||
|
||||
export const replace = (
|
||||
source: string | FmtString,
|
||||
search: string | RegExp,
|
||||
value: string | FmtString | ((...match: string[]) => string | FmtString),
|
||||
): FmtString => {
|
||||
source = FmtString.normalise(source);
|
||||
|
||||
let text = source.text;
|
||||
let entities: MessageEntity[] | undefined = source.entities;
|
||||
|
||||
function fixEntities(offset: number, length: number, correction: number) {
|
||||
const A = { offset, length };
|
||||
|
||||
return (entities || [])
|
||||
.map(E => {
|
||||
if (before(A, E)) return E;
|
||||
if (inside(A, E)) return;
|
||||
if (after(A, E)) return { ...E, offset: E.offset + correction };
|
||||
if (contains(A, E)) return { ...E, length: E.length + correction };
|
||||
if (endsInside(A, E))
|
||||
return { ...E, length: E.length - (ends(E) - starts(A)) };
|
||||
if (startsInside(A, E)) {
|
||||
const entityInside = ends(A) - starts(E);
|
||||
return {
|
||||
...E,
|
||||
offset: E.offset + entityInside + correction,
|
||||
length: E.length - entityInside,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Entity found in an unexpected condition. " +
|
||||
"This is probably a bug in telegraf. " +
|
||||
"You should report this to https://github.com/telegraf/telegraf/issues",
|
||||
);
|
||||
})
|
||||
.filter((x): x is MessageEntity => Boolean(x));
|
||||
}
|
||||
|
||||
if (typeof search === "string") {
|
||||
const replace = FmtString.normalise(
|
||||
typeof value === "function" ? value(...search) : value,
|
||||
);
|
||||
const offset = text.indexOf(search);
|
||||
const length = search.length;
|
||||
text = text.slice(0, offset) + replace.text + text.slice(offset + length);
|
||||
const currentCorrection = replace.text.length - length;
|
||||
entities = [
|
||||
...fixEntities(offset, length, currentCorrection),
|
||||
...(replace.entities || []).map(E => ({
|
||||
...E,
|
||||
offset: E.offset + offset,
|
||||
})),
|
||||
];
|
||||
} else {
|
||||
let index = 0; // context position in text string
|
||||
let acc = ""; // incremental return value
|
||||
let correction = 0;
|
||||
|
||||
let regexArray: RegExpExecArray | null;
|
||||
while ((regexArray = search.exec(text))) {
|
||||
const match = regexArray[0];
|
||||
const offset = regexArray.index;
|
||||
const length = match.length;
|
||||
const replace = FmtString.normalise(
|
||||
typeof value === "function" ? value(...regexArray) : value,
|
||||
);
|
||||
acc += text.slice(index, offset) + replace.text;
|
||||
const currentCorrection = replace.text.length - length;
|
||||
|
||||
entities = [
|
||||
...fixEntities(offset + correction, length, currentCorrection),
|
||||
...(replace.entities || []).map(E => ({
|
||||
...E,
|
||||
offset: E.offset + offset + correction,
|
||||
})),
|
||||
];
|
||||
|
||||
correction += currentCorrection;
|
||||
index = offset + length;
|
||||
}
|
||||
|
||||
text = acc + text.slice(index);
|
||||
}
|
||||
|
||||
return new FmtString(text, entities);
|
||||
};
|
66
src/index.ts
Normal file
66
src/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { ALLOWED_CHAT_IDS } from "../variables.js";
|
||||
import { ai, getVariant, newChat, setVariant, variants } from "./ai.js";
|
||||
import { checkOrigin } from "./check-origin.js";
|
||||
import { bot } from "./instances.js";
|
||||
import { message } from "telegraf/filters";
|
||||
import { useNewReplies } from "telegraf/future";
|
||||
|
||||
function args(cmd: string) {
|
||||
return cmd.split(" ").splice(1).join(" ");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
bot.use(useNewReplies());
|
||||
|
||||
if (typeof ALLOWED_CHAT_IDS != "string") {
|
||||
bot.use(checkOrigin(ALLOWED_CHAT_IDS));
|
||||
}
|
||||
|
||||
bot.command("ai", async ctx => {
|
||||
await ai(ctx, args(ctx.message.text));
|
||||
});
|
||||
|
||||
bot.on(message("reply_to_message"), async ctx => {
|
||||
if (ctx.message.reply_to_message.from?.id != ctx.botInfo.id) return;
|
||||
|
||||
const reply = ctx.message.reply_to_message;
|
||||
const message = ctx.message;
|
||||
if ("text" in reply && "text" in message) {
|
||||
await ai(ctx, message.text);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("newchat", async ctx => {
|
||||
newChat(ctx.chat.id);
|
||||
await ctx.reply(
|
||||
"The previous chat has been cleared. Any new /ai commands will continue a new chat.",
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("variant", async ctx => {
|
||||
const variant = args(ctx.message.text);
|
||||
|
||||
if (!variant)
|
||||
return await ctx.reply(
|
||||
`Variant for this chat is '${getVariant(ctx.chat.id)}'`,
|
||||
);
|
||||
|
||||
if (!variants.includes(variant))
|
||||
return await ctx.reply(
|
||||
`Invalid variant. Please use /help to learn about valid variants.`,
|
||||
);
|
||||
|
||||
setVariant(ctx.chat.id, variant);
|
||||
await ctx.reply(`The variant for this chat has been set to '${variant}'`);
|
||||
});
|
||||
|
||||
bot.launch();
|
||||
console.log("Bot running!");
|
||||
console.log("Use ^C to stop");
|
||||
|
||||
// Enable graceful stop
|
||||
process.once("SIGINT", () => bot.stop("SIGINT"));
|
||||
process.once("SIGTERM", () => bot.stop("SIGTERM"));
|
||||
}
|
||||
|
||||
main();
|
10
src/instances.ts
Normal file
10
src/instances.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { BING_COOKIE, TG_TOKEN } from "../variables.js";
|
||||
import { BingChat } from "bing-chat";
|
||||
import { Context, Telegraf } from "telegraf";
|
||||
import { Update } from "typegram";
|
||||
|
||||
export const bingChat = new BingChat({
|
||||
cookie: BING_COOKIE.replaceAll('"', "").trim(),
|
||||
});
|
||||
|
||||
export const bot: Telegraf<Context<Update>> = new Telegraf(TG_TOKEN.trim());
|
36
src/queue.ts
Normal file
36
src/queue.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
export const zerothPos = -1;
|
||||
export const firstPos = zerothPos + 1;
|
||||
|
||||
let clients = zerothPos;
|
||||
|
||||
export function queue() {
|
||||
const pos = ++clients;
|
||||
|
||||
return {
|
||||
pos,
|
||||
turn:
|
||||
pos == firstPos
|
||||
? // if this is the first client in the queue, no need to wait
|
||||
Promise.resolve(pos)
|
||||
: // otherwise, wait for our turn
|
||||
new Promise<number>(r => {
|
||||
const listener = (donePos: number) => {
|
||||
if (donePos == pos) {
|
||||
emitter.off("done", listener);
|
||||
r(pos);
|
||||
}
|
||||
};
|
||||
|
||||
emitter.on("done", listener);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function done() {
|
||||
if (clients == zerothPos) return;
|
||||
emitter.emit("done", clients--);
|
||||
}
|
69
src/transformers.ts
Normal file
69
src/transformers.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { replace } from "./fmt-replace.js";
|
||||
import { ChatMessage } from "bing-chat";
|
||||
import { FmtString, bold, code, fmt, italic, link, pre } from "telegraf/format";
|
||||
|
||||
type Transformers = ((reply: FmtString, res: ChatMessage) => FmtString)[];
|
||||
|
||||
export function transformBingResponse(res: ChatMessage) {
|
||||
return transformers.reduce(
|
||||
(text, transformer) => transformer(text, res),
|
||||
new FmtString(res.text),
|
||||
);
|
||||
}
|
||||
|
||||
export const transformers: Transformers = [
|
||||
function addNewChatSuffix(reply) {
|
||||
const lowercaseReply = reply.text.toLowerCase();
|
||||
const triggers = [
|
||||
"“new topic",
|
||||
"prefer not to continue",
|
||||
"still learning so I appreciate your understanding and patience",
|
||||
];
|
||||
|
||||
if (triggers.some(trigger => lowercaseReply.includes(trigger)))
|
||||
return fmt`${reply}\n\nUse /newchat to start a new topic.`;
|
||||
|
||||
return reply;
|
||||
},
|
||||
|
||||
function styleBold(reply) {
|
||||
return replace(reply, /\*\*(.*?)\*\*/g, (_, txt) => bold(txt));
|
||||
},
|
||||
|
||||
function styleItalic(reply) {
|
||||
return replace(reply, /_(.*?)_/g, (_, txt) => italic(txt));
|
||||
},
|
||||
|
||||
function styleAlternateItalic(reply) {
|
||||
return replace(reply, /\*(.*?)\*/g, (_, txt) => italic(txt));
|
||||
},
|
||||
|
||||
function styleCode(reply) {
|
||||
return replace(reply, /`(.*?)`/g, (_, codeString) => code(codeString));
|
||||
},
|
||||
|
||||
function styleReferences(reply, res) {
|
||||
const references = res.detail?.sourceAttributions;
|
||||
if (!references) return reply;
|
||||
|
||||
return replace(reply, /\[\^(.)\^\]/g, (_, oneBasedIndex) => {
|
||||
const referenceLink = references[parseInt(oneBasedIndex) - 1]?.seeMoreUrl;
|
||||
if (!referenceLink) return "";
|
||||
return fmt` ${bold(link(`[${oneBasedIndex}]`, referenceLink))}`;
|
||||
});
|
||||
},
|
||||
|
||||
function stylePre(reply) {
|
||||
return replace(
|
||||
reply,
|
||||
/```(\w+)\n([\s\S]*?)```/g,
|
||||
(_, language, codeString) => {
|
||||
return pre(language)(codeString);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
function styleAlternateBold(reply) {
|
||||
return replace(reply, /^`\n(.*?)`/gms, (_, txt) => bold(txt));
|
||||
},
|
||||
];
|
Reference in New Issue
Block a user