diff options
| author | Dhravya <[email protected]> | 2024-06-23 20:04:09 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-23 20:04:09 -0500 |
| commit | 51dd5ec9dd35d87c2e93a8e5a64fa963caaa182a (patch) | |
| tree | bbd6a2d0d1029c5aedcd2a7121987083649598d7 /apps/web/app/api | |
| parent | added backend route for telegram bot and others to be possible (diff) | |
| download | supermemory-51dd5ec9dd35d87c2e93a8e5a64fa963caaa182a.tar.xz supermemory-51dd5ec9dd35d87c2e93a8e5a64fa963caaa182a.zip | |
made and documented the telegram bot (HYPE)
Diffstat (limited to 'apps/web/app/api')
| -rw-r--r-- | apps/web/app/api/telegram/readme.md | 55 | ||||
| -rw-r--r-- | apps/web/app/api/telegram/route.ts | 89 |
2 files changed, 141 insertions, 3 deletions
diff --git a/apps/web/app/api/telegram/readme.md b/apps/web/app/api/telegram/readme.md new file mode 100644 index 00000000..79bd6f1c --- /dev/null +++ b/apps/web/app/api/telegram/readme.md @@ -0,0 +1,55 @@ +## how telegram bot stuff works + +### Let's start with the important bit: authentication. + +We wanted to find a good and secure way to authenticate users, or "link their supermemory account" to their telegram account. This was kinda challenging - because the requirements were tight and privacy was a big concern. + +1. No personally identifiable information should be stored, except the user's telegram ID and supermemory email. +2. The link should be as simple as a click of a button +3. it should work two-ways: If the user signs in to the website first, or uses the telegram bot first. +4. The user should be able to unlink their account at any time. +5. Should be very, very easy to host the telegram bot. + +We started out by trying to mingle with next-auth credentials provider - but that was a dead end. It would _work_, but would be too hard for us to implement and maintain, and would be a very bad user experience (get the token, copy it, paste it, etc). + +So we decided to go with a simple, yet secure, way of doing it. + +### the solution + +Well, the solution is simple af, surprisingly. To meet all these requirements, + +First off, we used the `grammy` library to create a telegram bot that works using websockets. (so, it's hosted with the website, and doesn't need a separate server) + +Now, let's examine both the flows: + +1. User signs in to the website first +2. Saves a bunch of stuff +3. wants to link their telegram account + +and... + +1. User uses the telegram bot first +2. Saves a bunch of stuff +3. wants to see their stuff in the supermemory account. + +What we ended up doing is creating a simple, yet secure way - always require signin through supermemory.ai website. +And if the user comes from the telegram bot, we just redirect them to the website with a token in the URL. + +The token. + +The token is literally just their telegram ID, but encrypted. We use a simple encryption algorithm to encrypt the telegram ID, and then decrypt it on the website. + +Why encryption? Because we don't want any random person to link any telegram account with their user id. The encryption is also interesting, done using an algorithm called [hushh](https://github.com/dhravya/hushh) that I made a while ago. It's simple and secure and all that's really needed is a secret key. + +Once the user signs in, we take the decrypted token and link it to their account. And that's it. The user can now use the telegram bot to access their stuff. Because it's on the same codebase on the server side, it's very easy to make database calls and also calls to the cf-ai-backend to generate stuff. + +### Natural language generation + +I wanted to add this: the bot actually does both - adding content and talking to the user - at the same time. + +How tho? +We use function calling in the backend repo smartly to decide what the user's intent would be. So, i can literally send the message "yo, can you remember this? (with anything else, can even be a URL!)" and the bot will understand that it's a command to add content. + +orr, i can send "hey, can you tell me about the time i went to the beach?" and the bot will understand that it's a command to get content. + +it's pretty cool. function calling using a cheap model works very well. diff --git a/apps/web/app/api/telegram/route.ts b/apps/web/app/api/telegram/route.ts index b80f6173..065a102a 100644 --- a/apps/web/app/api/telegram/route.ts +++ b/apps/web/app/api/telegram/route.ts @@ -1,3 +1,7 @@ +import { db } from "@/server/db"; +import { storedContent, users } from "@/server/db/schema"; +import { cipher, decipher } from "@/server/encrypt"; +import { eq } from "drizzle-orm"; import { Bot, webhookCallback } from "grammy"; import { User } from "grammy/types"; @@ -12,17 +16,96 @@ const token = process.env.TELEGRAM_BOT_TOKEN; const bot = new Bot(token); +const getUserByTelegramId = async (telegramId: string) => { + return await db.query.users + .findFirst({ + where: eq(users.telegramId, telegramId), + }) + .execute(); +}; + bot.command("start", async (ctx) => { const user: User = (await ctx.getAuthor()).user; + const cipherd = cipher(user.id.toString()); await ctx.reply( - `Welcome to Supermemory bot, ${user.first_name}. I am here to help you remember things better.`, + `Welcome to Supermemory bot. I am here to help you remember things better. Click here to create and link your accont: http://localhost:3000/signin?telegramUser=${cipherd}`, ); }); bot.on("message", async (ctx) => { - await ctx.reply( - "Hi there! This is Supermemory bot. I am here to help you remember things better.", + const user: User = (await ctx.getAuthor()).user; + const cipherd = cipher(user.id.toString()); + + const dbUser = await getUserByTelegramId(user.id.toString()); + + if (!dbUser) { + await ctx.reply( + `Welcome to Supermemory bot. I am here to help you remember things better. Click here to create and link your accont: http://localhost:3000/signin?telegramUser=${cipherd}`, + ); + + return; + } + + const message = await ctx.reply("I'm thinking..."); + + const response = await fetch( + `${process.env.BACKEND_BASE_URL}/api/autoChatOrAdd?query=${ctx.message.text}&user=${dbUser.id}`, + { + method: "POST", + headers: { + Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // TODO: we can use the conversations API to get the last 5 messages + // get chatHistory from this conversation. + // Basically the last 5 messages between the user and the assistant. + // In ths form of [{role: 'user' | 'assistant', content: string}] + // https://grammy.dev/plugins/conversations + chatHistory: [], + }), + }, ); + + if (response.status !== 200) { + console.log("Failed to get response from backend"); + console.log(response.status); + console.log(await response.text()); + await ctx.reply( + "Sorry, I am not able to process your request at the moment.", + ); + return; + } + + const data = (await response.json()) as { + status: string; + response: string; + contentAdded: { + type: string; + content: string; + url: string; + }; + }; + + // TODO: we might want to enrich this data with more information + if (data.contentAdded) { + await db + .insert(storedContent) + .values({ + content: data.contentAdded.content, + title: `${data.contentAdded.content.slice(0, 30)}... (Added from chatbot)`, + description: "", + url: data.contentAdded.url, + baseUrl: data.contentAdded.url, + image: "", + savedAt: new Date(), + userId: dbUser.id, + type: data.contentAdded.type, + }) + .returning({ id: storedContent.id }); + } + + await ctx.api.editMessageText(ctx.chat.id, message.message_id, data.response); }); export const POST = webhookCallback(bot, "std/http"); |