aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/api
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/api')
-rw-r--r--apps/web/app/api/[...nextauth]/route.ts2
-rw-r--r--apps/web/app/api/chat/route.ts100
-rw-r--r--apps/web/app/api/editorai/route.ts30
-rw-r--r--apps/web/app/api/ensureAuth.ts4
-rw-r--r--apps/web/app/api/getCount/route.ts8
-rw-r--r--apps/web/app/api/me/route.ts4
-rw-r--r--apps/web/app/api/spaces/route.ts4
-rw-r--r--apps/web/app/api/store/route.ts121
-rw-r--r--apps/web/app/api/telegram/readme.md55
-rw-r--r--apps/web/app/api/telegram/route.ts113
-rw-r--r--apps/web/app/api/unfirlsite/route.ts156
11 files changed, 422 insertions, 175 deletions
diff --git a/apps/web/app/api/[...nextauth]/route.ts b/apps/web/app/api/[...nextauth]/route.ts
index 50807ab1..e19cc16e 100644
--- a/apps/web/app/api/[...nextauth]/route.ts
+++ b/apps/web/app/api/[...nextauth]/route.ts
@@ -1,2 +1,2 @@
-export { GET, POST } from "../../helpers/server/auth";
+export { GET, POST } from "../../../server/auth";
export const runtime = "edge";
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index 34099848..d1730baa 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -1,6 +1,12 @@
import { type NextRequest } from "next/server";
-import { ChatHistory } from "@repo/shared-types";
+import {
+ ChatHistory,
+ ChatHistoryZod,
+ convertChatHistoryList,
+ SourcesFromApi,
+} from "@repo/shared-types";
import { ensureAuth } from "../ensureAuth";
+import { z } from "zod";
export const runtime = "edge";
@@ -15,59 +21,67 @@ export async function POST(req: NextRequest) {
return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 });
}
- const query = new URL(req.url).searchParams.get("q");
- const spaces = new URL(req.url).searchParams.get("spaces");
+ const url = new URL(req.url);
- const sourcesOnly =
- new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
+ const query = url.searchParams.get("q");
+ const spaces = url.searchParams.get("spaces");
- const chatHistory = (await req.json()) as {
+ const sourcesOnly = url.searchParams.get("sourcesOnly") ?? "false";
+
+ const jsonRequest = (await req.json()) as {
chatHistory: ChatHistory[];
+ sources: SourcesFromApi[] | undefined;
};
+ const { chatHistory, sources } = jsonRequest;
- console.log("CHathistory", chatHistory);
-
- if (!query) {
+ if (!query || query.trim.length < 0) {
return new Response(JSON.stringify({ message: "Invalid query" }), {
status: 400,
});
}
- try {
- const resp = await fetch(
- `https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
- {
- headers: {
- "X-Custom-Auth-Key": process.env.BACKEND_SECURITY_KEY!,
- },
- method: "POST",
- body: JSON.stringify({
- chatHistory: chatHistory.chatHistory ?? [],
- }),
+ const validated = z.array(ChatHistoryZod).safeParse(chatHistory ?? []);
+
+ if (!validated.success) {
+ return new Response(
+ JSON.stringify({
+ message: "Invalid chat history",
+ error: validated.error,
+ }),
+ { status: 400 },
+ );
+ }
+
+ const modelCompatible = await convertChatHistoryList(validated.data);
+
+ const resp = await fetch(
+ `${process.env.BACKEND_BASE_URL}/api/chat?query=${query}&user=${session.user.id}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
+ {
+ headers: {
+ Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
+ "Content-Type": "application/json",
},
+ method: "POST",
+ body: JSON.stringify({
+ chatHistory: modelCompatible,
+ sources,
+ }),
+ },
+ );
+
+ if (sourcesOnly == "true") {
+ const data = (await resp.json()) as SourcesFromApi;
+ return new Response(JSON.stringify(data), { status: 200 });
+ }
+
+ if (resp.status !== 200 || !resp.ok) {
+ const errorData = await resp.text();
+ console.log(errorData);
+ return new Response(
+ JSON.stringify({ message: "Error in CF function", error: errorData }),
+ { status: resp.status },
);
+ }
- console.log("sourcesOnly", sourcesOnly);
-
- if (sourcesOnly == "true") {
- const data = await resp.json();
- console.log("data", data);
- return new Response(JSON.stringify(data), { status: 200 });
- }
-
- if (resp.status !== 200 || !resp.ok) {
- const errorData = await resp.json();
- console.log(errorData);
- return new Response(
- JSON.stringify({ message: "Error in CF function", error: errorData }),
- { status: resp.status },
- );
- }
-
- // Stream the response back to the client
- const { readable, writable } = new TransformStream();
- resp && resp.body!.pipeTo(writable);
-
- return new Response(readable, { status: 200 });
- } catch {}
+ return new Response(resp.body, { status: 200 });
}
diff --git a/apps/web/app/api/editorai/route.ts b/apps/web/app/api/editorai/route.ts
new file mode 100644
index 00000000..5e1fbf0c
--- /dev/null
+++ b/apps/web/app/api/editorai/route.ts
@@ -0,0 +1,30 @@
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+// ERROR #2 - This the the next function that calls the backend, I sometimes think this is redundency, but whatever
+// I have commented the auth code, It should not work in development, but it still does sometimes
+export async function POST(request: NextRequest) {
+ // const d = await ensureAuth(request);
+ // if (!d) {
+ // return new Response("Unauthorized", { status: 401 });
+ // }
+ const res : {context: string, request: string} = await request.json()
+
+ try {
+ const resp = await fetch(`${process.env.BACKEND_BASE_URL}/api/editorai?context=${res.context}&request=${res.request}`);
+ // this just checks if there are erros I am keeping it commented for you to better understand the important pieces
+ // if (resp.status !== 200 || !resp.ok) {
+ // const errorData = await resp.text();
+ // console.log(errorData);
+ // return new Response(
+ // JSON.stringify({ message: "Error in CF function", error: errorData }),
+ // { status: resp.status },
+ // );
+ // }
+ return new Response(resp.body, { status: 200 });
+ } catch (error) {
+ return new Response(`Error, ${error}`)
+ }
+} \ No newline at end of file
diff --git a/apps/web/app/api/ensureAuth.ts b/apps/web/app/api/ensureAuth.ts
index a1401a07..d2fbac0b 100644
--- a/apps/web/app/api/ensureAuth.ts
+++ b/apps/web/app/api/ensureAuth.ts
@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
-import { db } from "../helpers/server/db";
-import { sessions, users } from "../helpers/server/db/schema";
+import { db } from "../../server/db";
+import { sessions, users } from "../../server/db/schema";
import { eq } from "drizzle-orm";
export async function ensureAuth(req: NextRequest) {
diff --git a/apps/web/app/api/getCount/route.ts b/apps/web/app/api/getCount/route.ts
index f760c145..7cd2a2d3 100644
--- a/apps/web/app/api/getCount/route.ts
+++ b/apps/web/app/api/getCount/route.ts
@@ -1,6 +1,6 @@
-import { db } from "@/app/helpers/server/db";
+import { db } from "@/server/db";
import { and, eq, ne, sql } from "drizzle-orm";
-import { sessions, storedContent, users } from "@/app/helpers/server/db/schema";
+import { sessions, storedContent, users } from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
import { ensureAuth } from "../ensureAuth";
@@ -20,7 +20,7 @@ export async function GET(req: NextRequest) {
.from(storedContent)
.where(
and(
- eq(storedContent.user, session.user.id),
+ eq(storedContent.userId, session.user.id),
eq(storedContent.type, "twitter-bookmark"),
),
);
@@ -32,7 +32,7 @@ export async function GET(req: NextRequest) {
.from(storedContent)
.where(
and(
- eq(storedContent.user, session.user.id),
+ eq(storedContent.userId, session.user.id),
ne(storedContent.type, "twitter-bookmark"),
),
);
diff --git a/apps/web/app/api/me/route.ts b/apps/web/app/api/me/route.ts
index 20b6aece..621dcbfe 100644
--- a/apps/web/app/api/me/route.ts
+++ b/apps/web/app/api/me/route.ts
@@ -1,6 +1,6 @@
-import { db } from "@/app/helpers/server/db";
+import { db } from "@/server/db";
import { eq } from "drizzle-orm";
-import { sessions, users } from "@/app/helpers/server/db/schema";
+import { sessions, users } from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
export const runtime = "edge";
diff --git a/apps/web/app/api/spaces/route.ts b/apps/web/app/api/spaces/route.ts
index c46b02fc..cbed547d 100644
--- a/apps/web/app/api/spaces/route.ts
+++ b/apps/web/app/api/spaces/route.ts
@@ -1,5 +1,5 @@
-import { db } from "@/app/helpers/server/db";
-import { sessions, space, users } from "@/app/helpers/server/db/schema";
+import { db } from "@/server/db";
+import { sessions, space, users } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
import { ensureAuth } from "../ensureAuth";
diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts
deleted file mode 100644
index f96f90cf..00000000
--- a/apps/web/app/api/store/route.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { db } from "@/app/helpers/server/db";
-import { and, eq, sql, inArray } from "drizzle-orm";
-import {
- contentToSpace,
- sessions,
- storedContent,
- users,
- space,
-} from "@/app/helpers/server/db/schema";
-import { type NextRequest, NextResponse } from "next/server";
-import { getMetaData } from "@/app/helpers/lib/get-metadata";
-import { ensureAuth } from "../ensureAuth";
-
-export const runtime = "edge";
-
-export async function POST(req: NextRequest) {
- const session = await ensureAuth(req);
-
- if (!session) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- const data = (await req.json()) as {
- pageContent: string;
- url: string;
- spaces?: string[];
- };
-
- const metadata = await getMetaData(data.url);
- let storeToSpaces = data.spaces;
-
- if (!storeToSpaces) {
- storeToSpaces = [];
- }
-
- const count = await db
- .select({
- count: sql<number>`count(*)`.mapWith(Number),
- })
- .from(storedContent)
- .where(
- and(
- eq(storedContent.user, session.user.id),
- eq(storedContent.type, "page"),
- ),
- );
-
- if (count[0]!.count > 100) {
- return NextResponse.json(
- { message: "Error", error: "Limit exceeded" },
- { status: 499 },
- );
- }
-
- const rep = await db
- .insert(storedContent)
- .values({
- content: data.pageContent,
- title: metadata.title,
- description: metadata.description,
- url: data.url,
- baseUrl: metadata.baseUrl,
- image: metadata.image,
- savedAt: new Date(),
- user: session.user.id,
- })
- .returning({ id: storedContent.id });
-
- const id = rep[0]?.id;
-
- if (!id) {
- return NextResponse.json(
- { message: "Error", error: "Error in CF function" },
- { status: 500 },
- );
- }
-
- if (storeToSpaces.length > 0) {
- const spaceData = await db
- .select()
- .from(space)
- .where(
- and(
- inArray(space.name, storeToSpaces ?? []),
- eq(space.user, session.user.id),
- ),
- )
- .all();
-
- await Promise.all([
- spaceData.forEach(async (space) => {
- await db
- .insert(contentToSpace)
- .values({ contentId: id, spaceId: space.id });
- }),
- ]);
- }
-
- const res = (await Promise.race([
- fetch("https://cf-ai-backend.dhravya.workers.dev/add", {
- method: "POST",
- headers: {
- "X-Custom-Auth-Key": process.env.BACKEND_SECURITY_KEY,
- },
- body: JSON.stringify({ ...data, user: session.user.email }),
- }),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error("Request timed out")), 40000),
- ),
- ])) as Response;
-
- if (res.status !== 200) {
- console.log(res.status, res.statusText);
- return NextResponse.json(
- { message: "Error", error: "Error in CF function" },
- { status: 500 },
- );
- }
-
- return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 });
-}
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
new file mode 100644
index 00000000..c6c673b2
--- /dev/null
+++ b/apps/web/app/api/telegram/route.ts
@@ -0,0 +1,113 @@
+import { db } from "@/server/db";
+import { storedContent, users } from "@/server/db/schema";
+import { cipher } from "@/server/encrypt";
+import { eq } from "drizzle-orm";
+import { Bot, webhookCallback } from "grammy";
+import { User } from "grammy/types";
+
+export const runtime = "edge";
+
+if (!process.env.TELEGRAM_BOT_TOKEN) {
+ throw new Error("TELEGRAM_BOT_TOKEN is not defined");
+}
+
+console.log("Telegram bot activated");
+const token = process.env.TELEGRAM_BOT_TOKEN;
+
+const bot = new Bot(token);
+
+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. I am here to help you remember things better. Click here to create and link your account: https://beta.supermemory.ai/signin?telegramUser=${cipherd}`,
+ );
+});
+
+bot.on("message", async (ctx) => {
+ const user: User = (await ctx.getAuthor()).user;
+
+ const cipherd = cipher(user.id.toString());
+
+ const dbUser = await db.query.users
+ .findFirst({
+ where: eq(users.telegramId, user.id.toString()),
+ })
+ .execute();
+
+ 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 account: https://beta.supermemory.ai/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");
+
+export const GET = async () => {
+ return new Response("OK", { status: 200 });
+};
diff --git a/apps/web/app/api/unfirlsite/route.ts b/apps/web/app/api/unfirlsite/route.ts
new file mode 100644
index 00000000..36e47987
--- /dev/null
+++ b/apps/web/app/api/unfirlsite/route.ts
@@ -0,0 +1,156 @@
+import { load } from "cheerio";
+import { AwsClient } from "aws4fetch";
+
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+export async function POST(request: NextRequest) {
+ const r2 = new AwsClient({
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
+ });
+
+ async function unfurl(url: string) {
+ const response = await fetch(url);
+ if (response.status >= 400) {
+ throw new Error(`Error fetching url: ${response.status}`);
+ }
+ const contentType = response.headers.get("content-type");
+ if (!contentType?.includes("text/html")) {
+ throw new Error(`Content-type not right: ${contentType}`);
+ }
+
+ const content = await response.text();
+ const $ = load(content);
+
+ const og: { [key: string]: string | undefined } = {};
+ const twitter: { [key: string]: string | undefined } = {};
+
+ $("meta[property^=og:]").each(
+ // @ts-ignore, it just works so why care of type safety if someone has better way go ahead
+ (_, el) => (og[$(el).attr("property")!] = $(el).attr("content")),
+ );
+ $("meta[name^=twitter:]").each(
+ // @ts-ignore
+ (_, el) => (twitter[$(el).attr("name")!] = $(el).attr("content")),
+ );
+
+ const title =
+ og["og:title"] ??
+ twitter["twitter:title"] ??
+ $("title").text() ??
+ undefined;
+ const description =
+ og["og:description"] ??
+ twitter["twitter:description"] ??
+ $('meta[name="description"]').attr("content") ??
+ undefined;
+ const image =
+ og["og:image:secure_url"] ??
+ og["og:image"] ??
+ twitter["twitter:image"] ??
+ undefined;
+
+ return {
+ title,
+ description,
+ image,
+ };
+ }
+
+ const d = await ensureAuth(request);
+ if (!d) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ if (
+ !process.env.R2_ACCESS_KEY_ID ||
+ !process.env.R2_ACCOUNT_ID ||
+ !process.env.R2_SECRET_ACCESS_KEY ||
+ !process.env.R2_BUCKET_NAME
+ ) {
+ return new Response(
+ "Missing one or more R2 env variables: R2_ENDPOINT, R2_ACCESS_ID, R2_SECRET_KEY, R2_BUCKET_NAME. To get them, go to the R2 console, create and paste keys in a `.dev.vars` file in the root of this project.",
+ { status: 500 },
+ );
+ }
+
+ const website = new URL(request.url).searchParams.get("website");
+
+ if (!website) {
+ return new Response("Missing website", { status: 400 });
+ }
+
+ const salt = () => Math.floor(Math.random() * 11);
+ const encodeWebsite = `${encodeURIComponent(website)}${salt()}`;
+
+ try {
+ // this returns the og image, description and title of website
+ const response = await unfurl(website);
+
+ if (!response.image) {
+ return new Response(JSON.stringify(response));
+ }
+
+ if (!process.env.DEV_IMAGES) {
+ return new Response("Missing DEV_IMAGES namespace.", { status: 500 });
+ }
+
+ const imageUrl = await process.env.DEV_IMAGES!.get(encodeWebsite);
+ if (imageUrl) {
+ return new Response(
+ JSON.stringify({
+ image: imageUrl,
+ title: response.title,
+ description: response.description,
+ }),
+ );
+ }
+
+ const res = await fetch(`${response.image}`);
+ const image = await res.blob();
+
+ const url = new URL(
+ `https://${process.env.R2_BUCKET_NAME}.${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
+ );
+
+ url.pathname = encodeWebsite;
+ url.searchParams.set("X-Amz-Expires", "3600");
+
+ const signedPuturl = await r2.sign(
+ new Request(url, {
+ method: "PUT",
+ }),
+ {
+ aws: { signQuery: true },
+ },
+ );
+ await fetch(signedPuturl.url, {
+ method: "PUT",
+ body: image,
+ });
+
+ await process.env.DEV_IMAGES.put(
+ encodeWebsite,
+ `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`,
+ );
+
+ return new Response(
+ JSON.stringify({
+ image: `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`,
+ title: response.title,
+ description: response.description,
+ }),
+ );
+ } catch (error) {
+ console.log(error);
+ return new Response(
+ JSON.stringify({
+ status: 500,
+ error: error,
+ }),
+ );
+ }
+}