This commit is contained in:
Udit Karode
2023-04-08 10:17:49 +05:30
commit 77c704db49
14 changed files with 821 additions and 0 deletions

72
src/ai.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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));
},
];