Spaces:
Paused
Paused
very basic rate limiter (#320)
Browse files- .env +1 -0
- src/lib/server/database.ts +4 -0
- src/lib/stores/errors.ts +1 -0
- src/lib/types/MessageEvent.ts +6 -0
- src/routes/conversation/[id]/+page.svelte +2 -0
- src/routes/conversation/[id]/+server.ts +19 -0
.env
CHANGED
|
@@ -70,6 +70,7 @@ PARQUET_EXPORT_DATASET=
|
|
| 70 |
PARQUET_EXPORT_HF_TOKEN=
|
| 71 |
PARQUET_EXPORT_SECRET=
|
| 72 |
|
|
|
|
| 73 |
|
| 74 |
PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
|
| 75 |
PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS
|
|
|
|
| 70 |
PARQUET_EXPORT_HF_TOKEN=
|
| 71 |
PARQUET_EXPORT_SECRET=
|
| 72 |
|
| 73 |
+
RATE_LIMIT= # requests per minute
|
| 74 |
|
| 75 |
PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
|
| 76 |
PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS
|
src/lib/server/database.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { WebSearch } from "$lib/types/WebSearch";
|
|
| 6 |
import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
|
| 7 |
import type { Settings } from "$lib/types/Settings";
|
| 8 |
import type { User } from "$lib/types/User";
|
|
|
|
| 9 |
|
| 10 |
if (!MONGODB_URL) {
|
| 11 |
throw new Error(
|
|
@@ -27,6 +28,7 @@ const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations"
|
|
| 27 |
const settings = db.collection<Settings>("settings");
|
| 28 |
const users = db.collection<User>("users");
|
| 29 |
const webSearches = db.collection<WebSearch>("webSearches");
|
|
|
|
| 30 |
|
| 31 |
export { client, db };
|
| 32 |
export const collections = {
|
|
@@ -36,6 +38,7 @@ export const collections = {
|
|
| 36 |
settings,
|
| 37 |
users,
|
| 38 |
webSearches,
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
client.on("open", () => {
|
|
@@ -59,4 +62,5 @@ client.on("open", () => {
|
|
| 59 |
settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(console.error);
|
| 60 |
users.createIndex({ hfUserId: 1 }, { unique: true }).catch(console.error);
|
| 61 |
users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
|
|
|
|
| 62 |
});
|
|
|
|
| 6 |
import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
|
| 7 |
import type { Settings } from "$lib/types/Settings";
|
| 8 |
import type { User } from "$lib/types/User";
|
| 9 |
+
import type { MessageEvent } from "$lib/types/MessageEvent";
|
| 10 |
|
| 11 |
if (!MONGODB_URL) {
|
| 12 |
throw new Error(
|
|
|
|
| 28 |
const settings = db.collection<Settings>("settings");
|
| 29 |
const users = db.collection<User>("users");
|
| 30 |
const webSearches = db.collection<WebSearch>("webSearches");
|
| 31 |
+
const messageEvents = db.collection<MessageEvent>("messageEvents");
|
| 32 |
|
| 33 |
export { client, db };
|
| 34 |
export const collections = {
|
|
|
|
| 38 |
settings,
|
| 39 |
users,
|
| 40 |
webSearches,
|
| 41 |
+
messageEvents,
|
| 42 |
};
|
| 43 |
|
| 44 |
client.on("open", () => {
|
|
|
|
| 62 |
settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(console.error);
|
| 63 |
users.createIndex({ hfUserId: 1 }, { unique: true }).catch(console.error);
|
| 64 |
users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
|
| 65 |
+
messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error);
|
| 66 |
});
|
src/lib/stores/errors.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { writable } from "svelte/store";
|
|
| 3 |
export const ERROR_MESSAGES = {
|
| 4 |
default: "Oops, something went wrong.",
|
| 5 |
authOnly: "You have to be logged in.",
|
|
|
|
| 6 |
};
|
| 7 |
|
| 8 |
export const error = writable<string | null>(null);
|
|
|
|
| 3 |
export const ERROR_MESSAGES = {
|
| 4 |
default: "Oops, something went wrong.",
|
| 5 |
authOnly: "You have to be logged in.",
|
| 6 |
+
rateLimited: "You are sending too many messages. Try again later.",
|
| 7 |
};
|
| 8 |
|
| 9 |
export const error = writable<string | null>(null);
|
src/lib/types/MessageEvent.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Timestamps } from "./Timestamps";
|
| 2 |
+
import type { User } from "./User";
|
| 3 |
+
|
| 4 |
+
export interface MessageEvent extends Pick<Timestamps, "createdAt"> {
|
| 5 |
+
userId: User["_id"] | User["sessionId"];
|
| 6 |
+
}
|
src/routes/conversation/[id]/+page.svelte
CHANGED
|
@@ -207,6 +207,8 @@
|
|
| 207 |
} catch (err) {
|
| 208 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
| 209 |
$error = "Too much traffic, please try again.";
|
|
|
|
|
|
|
| 210 |
} else if (err instanceof Error) {
|
| 211 |
$error = err.message;
|
| 212 |
} else {
|
|
|
|
| 207 |
} catch (err) {
|
| 208 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
| 209 |
$error = "Too much traffic, please try again.";
|
| 210 |
+
} else if (err instanceof Error && err.message.includes("429")) {
|
| 211 |
+
$error = ERROR_MESSAGES.rateLimited;
|
| 212 |
} else if (err instanceof Error) {
|
| 213 |
$error = err.message;
|
| 214 |
} else {
|
src/routes/conversation/[id]/+server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 2 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
| 3 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
|
@@ -5,6 +6,7 @@ import { authCondition } from "$lib/server/auth";
|
|
| 5 |
import { collections } from "$lib/server/database";
|
| 6 |
import { modelEndpoint } from "$lib/server/modelEndpoint";
|
| 7 |
import { models } from "$lib/server/models";
|
|
|
|
| 8 |
import type { Message } from "$lib/types/Message";
|
| 9 |
import { concatUint8Arrays } from "$lib/utils/concatUint8Arrays";
|
| 10 |
import { streamToAsyncIterable } from "$lib/utils/streamToAsyncIterable";
|
|
@@ -20,6 +22,12 @@ export async function POST({ request, fetch, locals, params }) {
|
|
| 20 |
const convId = new ObjectId(id);
|
| 21 |
const date = new Date();
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const conv = await collections.conversations.findOne({
|
| 24 |
_id: convId,
|
| 25 |
...authCondition(locals),
|
|
@@ -29,6 +37,12 @@ export async function POST({ request, fetch, locals, params }) {
|
|
| 29 |
throw error(404, "Conversation not found");
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
const model = models.find((m) => m.id === conv.model);
|
| 33 |
|
| 34 |
if (!model) {
|
|
@@ -118,6 +132,11 @@ export async function POST({ request, fetch, locals, params }) {
|
|
| 118 |
id: (responseId as Message["id"]) || crypto.randomUUID(),
|
| 119 |
});
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
await collections.conversations.updateOne(
|
| 122 |
{
|
| 123 |
_id: convId,
|
|
|
|
| 1 |
+
import { RATE_LIMIT } from "$env/static/private";
|
| 2 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 3 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
| 4 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
|
|
|
| 6 |
import { collections } from "$lib/server/database";
|
| 7 |
import { modelEndpoint } from "$lib/server/modelEndpoint";
|
| 8 |
import { models } from "$lib/server/models";
|
| 9 |
+
import { ERROR_MESSAGES } from "$lib/stores/errors.js";
|
| 10 |
import type { Message } from "$lib/types/Message";
|
| 11 |
import { concatUint8Arrays } from "$lib/utils/concatUint8Arrays";
|
| 12 |
import { streamToAsyncIterable } from "$lib/utils/streamToAsyncIterable";
|
|
|
|
| 22 |
const convId = new ObjectId(id);
|
| 23 |
const date = new Date();
|
| 24 |
|
| 25 |
+
const userId = locals.user?._id ?? locals.sessionId;
|
| 26 |
+
|
| 27 |
+
if (!userId) {
|
| 28 |
+
throw error(401, "Unauthorized");
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
const conv = await collections.conversations.findOne({
|
| 32 |
_id: convId,
|
| 33 |
...authCondition(locals),
|
|
|
|
| 37 |
throw error(404, "Conversation not found");
|
| 38 |
}
|
| 39 |
|
| 40 |
+
const nEvents = await collections.messageEvents.countDocuments({ userId });
|
| 41 |
+
|
| 42 |
+
if (RATE_LIMIT != "" && nEvents > parseInt(RATE_LIMIT)) {
|
| 43 |
+
throw error(429, ERROR_MESSAGES.rateLimited);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
const model = models.find((m) => m.id === conv.model);
|
| 47 |
|
| 48 |
if (!model) {
|
|
|
|
| 132 |
id: (responseId as Message["id"]) || crypto.randomUUID(),
|
| 133 |
});
|
| 134 |
|
| 135 |
+
await collections.messageEvents.insertOne({
|
| 136 |
+
userId: userId,
|
| 137 |
+
createdAt: new Date(),
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
await collections.conversations.updateOne(
|
| 141 |
{
|
| 142 |
_id: convId,
|