Merging from main

This commit is contained in:
Keith Stevens
2023-01-28 18:05:56 +09:00
188 changed files with 5516 additions and 2442 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ export const post = (url: string, { arg: data }) => api.post(url, data).then((re
api.interceptors.response.use(
(response) => response,
(error) => {
throw new OasstError(error.message ?? error, error.error_code);
const err = error?.response?.data;
throw new OasstError(err?.message ?? error, err?.errorCode, error?.response?.httpStatusCode || -1);
}
);
+2 -2
View File
@@ -21,14 +21,14 @@ const withoutRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiRe
* Wraps any API Route handler and verifies that the user has the appropriate
* role before running the handler. Returns a 403 otherwise.
*/
const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse) => void) => {
const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse, token: JWT) => void) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
const token = await getToken({ req });
if (!token || token.role !== role) {
res.status(403).end();
return;
}
return handler(req, res);
return handler(req, res, token);
};
};
+43
View File
@@ -0,0 +1,43 @@
import {
useCreateAssistantReply,
useCreateInitialPrompt,
useCreatePrompterReply,
} from "src/hooks/tasks/useCreateReply";
import { useGenericTaskAPI } from "src/hooks/tasks/useGenericTaskAPI";
import {
useLabelAssistantReplyTask,
useLabelInitialPromptTask,
useLabelPrompterReplyTask,
} from "src/hooks/tasks/useLabelingTask";
import {
useRankAssistantRepliesTask,
useRankInitialPromptsTask,
useRankPrompterRepliesTask,
} from "src/hooks/tasks/useRankReplies";
import { TaskApiHooks } from "src/types/Hooks";
import { TaskType } from "src/types/Task";
export const ERROR_CODES = {
TASK_REQUESTED_TYPE_NOT_AVAILABLE: 1006,
TASK_INVALID_REQUEST_TYPE: 1000,
TASK_ACK_FAILED: 1001,
TASK_NACK_FAILED: 1002,
TASK_INVALID_RESPONSE_TYPE: 1003,
TASK_INTERACTION_REQUEST_FAILED: 1004,
TASK_GENERATION_FAILED: 1005,
TASK_AVAILABILITY_QUERY_FAILED: 1007,
TASK_MESSAGE_TOO_LONG: 1008,
};
export const taskApiHooks: TaskApiHooks = {
[TaskType.random]: useGenericTaskAPI,
[TaskType.assistant_reply]: useCreateAssistantReply,
[TaskType.initial_prompt]: useCreateInitialPrompt,
[TaskType.label_assistant_reply]: useLabelAssistantReplyTask,
[TaskType.label_initial_prompt]: useLabelInitialPromptTask,
[TaskType.label_prompter_reply]: useLabelPrompterReplyTask,
[TaskType.prompter_reply]: useCreatePrompterReply,
[TaskType.rank_assistant_replies]: useRankAssistantRepliesTask,
[TaskType.rank_initial_prompts]: useRankInitialPromptsTask,
[TaskType.rank_prompter_replies]: useRankPrompterRepliesTask,
};
+1
View File
@@ -0,0 +1 @@
export const getTypeSafei18nKey = (key: string) => key as unknown as TemplateStringsArray;
+151 -124
View File
@@ -1,14 +1,14 @@
import type { Message } from "src/types/Conversation";
import type { EmojiOp, Message } from "src/types/Conversation";
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
import type { AvailableTasks } from "src/types/Task";
import type { BackendUser, BackendUserCore } from "src/types/Users";
import type { BackendUser, BackendUserCore, FetchUsersParams, FetchUsersResponse } from "src/types/Users";
export class OasstError {
message: string;
errorCode: number;
httpStatusCode: number;
constructor(message: string, errorCode: number, httpStatusCode?: number) {
constructor(message: string, errorCode: number, httpStatusCode: number) {
this.message = message;
this.errorCode = errorCode;
this.httpStatusCode = httpStatusCode;
@@ -18,110 +18,35 @@ export class OasstError {
export class OasstApiClient {
oasstApiUrl: string;
oasstApiKey: string;
userHeaders: Record<string, string> = {};
constructor(oasstApiUrl: string, oasstApiKey: string) {
constructor(oasstApiUrl: string, oasstApiKey: string, user?: BackendUserCore) {
this.oasstApiUrl = oasstApiUrl;
this.oasstApiKey = oasstApiKey;
if (user) {
this.userHeaders = {
"X-OASST-USER": `${user.auth_method}:${user.id}`,
};
}
}
private async post(path: string, body: any): Promise<any> {
const resp = await fetch(`${this.oasstApiUrl}${path}`, {
method: "POST",
headers: {
"X-API-Key": this.oasstApiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (resp.status === 204) {
return null;
}
if (resp.status >= 300) {
const errorText = await resp.text();
let error: any;
try {
error = JSON.parse(errorText);
} catch (e) {
throw new OasstError(errorText, 0, resp.status);
}
throw new OasstError(error.message ?? error, error.error_code, resp.status);
}
return await resp.json();
}
private async put(path: string): Promise<any> {
const resp = await fetch(`${this.oasstApiUrl}${path}`, {
method: "PUT",
headers: {
"X-API-Key": this.oasstApiKey,
},
});
if (resp.status === 204) {
return null;
}
if (resp.status >= 300) {
const errorText = await resp.text();
let error: any;
try {
error = JSON.parse(errorText);
} catch (e) {
throw new OasstError(errorText, 0, resp.status);
}
throw new OasstError(error.message ?? error, error.error_code, resp.status);
}
return await resp.json();
}
private async get(path: string): Promise<any> {
const resp = await fetch(`${this.oasstApiUrl}${path}`, {
method: "GET",
headers: {
"X-API-Key": this.oasstApiKey,
"Content-Type": "application/json",
},
});
if (resp.status === 204) {
return null;
}
if (resp.status >= 300) {
const errorText = await resp.text();
let error: any;
try {
error = JSON.parse(errorText);
} catch (e) {
throw new OasstError(errorText, 0, resp.status);
}
throw new OasstError(error.message ?? error, error.error_code, resp.status);
}
return await resp.json();
}
// TODO return a strongly typed Task?
// This method is used to store a task in RegisteredTask.task.
// This is a raw Json type, so we can't use it to strongly type the task.
async fetchTask(taskType: string, user: BackendUserCore): Promise<any> {
async fetchTask(taskType: string, user: BackendUserCore, lang: string): Promise<any> {
return this.post("/api/v1/tasks/", {
type: taskType,
user,
lang,
});
}
async ackTask(taskId: string, messageId: string): Promise<void> {
async ackTask(taskId: string, messageId: string): Promise<null> {
return this.post(`/api/v1/tasks/${taskId}/ack`, {
message_id: messageId,
});
}
async nackTask(taskId: string, reason: string): Promise<void> {
async nackTask(taskId: string, reason: string): Promise<null> {
return this.post(`/api/v1/tasks/${taskId}/nack`, {
reason,
});
@@ -136,7 +61,8 @@ export class OasstApiClient {
messageId: string,
userMessageId: string,
content: object,
user: BackendUserCore
user: BackendUserCore,
lang: string
): Promise<any> {
return this.post("/api/v1/tasks/interaction", {
type: updateType,
@@ -144,6 +70,7 @@ export class OasstApiClient {
task_id: taskId,
message_id: messageId,
user_message_id: userMessageId,
lang,
...content,
});
}
@@ -151,8 +78,29 @@ export class OasstApiClient {
/**
* Returns the tasks availability information for given `user`.
*/
async fetch_tasks_availability(user: object): Promise<any> {
return this.post("/api/v1/tasks/availability", user);
async fetch_tasks_availability(user: object): Promise<AvailableTasks | null> {
return this.post<AvailableTasks>("/api/v1/tasks/availability", user);
}
/**
* Returns the `Message`s associated with `user_id` in the backend.
*/
async fetch_message(message_id: string, user: BackendUserCore): Promise<Message> {
return this.get<Message>(`/api/v1/messages/${message_id}?username=${user.id}&auth_method=${user.auth_method}`);
}
/**
* Send a report about a message
*/
async send_report(message_id: string, user: BackendUserCore, text: string) {
return this.post("/api/v1/text_labels", {
type: "text_labels",
message_id,
labels: [], // Not yet implemented
text,
is_report: true,
user,
});
}
/**
@@ -172,46 +120,41 @@ export class OasstApiClient {
/**
* Returns the `BackendUser` associated with `user_id`
*/
async fetch_user(user_id: string): Promise<BackendUser> {
async fetch_user(user_id: string): Promise<BackendUser | null> {
return this.get(`/api/v1/users/${user_id}`);
}
/**
* Returns the set of `BackendUser`s stored by the backend.
*
* @param {number} max_count - The maximum number of users to fetch.
* @param {string} cursor - The user's `display_name` to use when paginating.
* @param {boolean} isForward - If true and `cursor` is not empty, pages
* forward. If false and `cursor` is not empty, pages backwards.
* @returns {Promise<BackendUser[]>} A Promise that returns an array of `BackendUser` objects.
*/
async fetch_users(max_count: number, cursor: string, isForward: boolean): Promise<BackendUser[]> {
const params = new URLSearchParams();
params.append("max_count", max_count.toString());
// The backend API uses different query parameters depending on the
// pagination direction but they both take the same cursor value.
// Depending on direction, pick the right query param.
if (cursor !== "") {
params.append(isForward ? "gt" : "lt", cursor);
}
const BASE_URL = `/api/v1/frontend_users`;
const url = `${BASE_URL}/?${params.toString()}`;
return this.get(url);
async fetch_users({
direction,
limit,
cursor,
searchDisplayName,
sortKey = "display_name",
}: FetchUsersParams): Promise<FetchUsersResponse | null> {
return this.get<FetchUsersResponse>(`/api/v1/users/cursor`, {
search_text: searchDisplayName,
sort_key: sortKey,
max_count: limit,
after: direction === "forward" ? cursor : undefined,
before: direction === "back" ? cursor : undefined,
});
}
/**
* Returns the `Message`s associated with `user_id` in the backend.
*/
async fetch_user_messages(user_id: string): Promise<Message[]> {
return this.get(`/api/v1/users/${user_id}/messages`);
async fetch_user_messages(user_id: string): Promise<Message[] | null> {
return this.get<Message[]>(`/api/v1/users/${user_id}/messages`);
}
/**
* Updates the backend's knowledge about the `user_id`.
*/
async set_user_status(user_id: string, is_enabled: boolean, notes): Promise<void> {
return this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}&notes=${notes}`);
async set_user_status(user_id: string, is_enabled: boolean, notes: string): Promise<void> {
await this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}&notes=${notes}`);
}
/**
@@ -224,18 +167,102 @@ export class OasstApiClient {
/**
* Returns the current leaderboard ranking.
*/
async fetch_leaderboard(time_frame: LeaderboardTimeFrame): Promise<LeaderboardReply> {
return this.get(`/api/v1/leaderboards/${time_frame}`);
async fetch_leaderboard(
time_frame: LeaderboardTimeFrame,
{ limit = 20 }: { limit?: number }
): Promise<LeaderboardReply | null> {
return this.get<LeaderboardReply>(`/api/v1/leaderboards/${time_frame}`, { max_count: limit });
}
/**
* Returns the counts of all tasks (some might be zero)
*/
async fetch_available_tasks(user: BackendUserCore): Promise<AvailableTasks> {
return this.post(`/api/v1/tasks/availability`, user);
async fetch_available_tasks(user: BackendUserCore, lang: string): Promise<AvailableTasks | null> {
return this.post<AvailableTasks>(`/api/v1/tasks/availability?lang=${lang}`, user);
}
/**
* Add/remove an emoji on a message for a user
*/
async set_user_message_emoji(message_id: string, user: BackendUserCore, emoji: string, op: EmojiOp): Promise<void> {
await this.post(`/api/v1/messages/${message_id}/emoji`, {
user,
emoji,
op,
});
}
private async post<T>(path: string, body: unknown) {
return this.request<T>("POST", path, {
body: JSON.stringify(body),
});
}
private async put<T>(path: string) {
return this.request<T>("PUT", path);
}
private async get<T>(path: string, query?: Record<string, string | number | boolean | undefined>) {
if (!query) {
return this.request<T>("GET", path);
}
const filteredQuery = Object.fromEntries(
Object.entries(query).filter(([, value]) => value !== undefined)
) as Record<string, string>;
const params = new URLSearchParams(filteredQuery).toString();
return this.request<T>("GET", `${path}?${params}`);
}
private async request<T>(method: "GET" | "POST" | "PUT", path: string, init?: RequestInit): Promise<T | null> {
const resp = await fetch(`${this.oasstApiUrl}${path}`, {
method,
...init,
headers: {
...init?.headers,
...this.userHeaders,
"X-API-Key": this.oasstApiKey,
"Content-Type": "application/json",
},
});
if (resp.status === 204) {
return null;
}
if (resp.status >= 300) {
const errorText = await resp.text();
let error;
try {
error = JSON.parse(errorText);
} catch (e) {
throw new OasstError(errorText, 0, resp.status);
}
throw new OasstError(error.message ?? error, error.error_code, resp.status);
}
return await resp.json();
}
fetch_my_messages(user: BackendUserCore) {
const params = new URLSearchParams({
username: user.id,
auth_method: user.auth_method,
});
return this.get<Message[]>(`/api/v1/messages?${params}`);
}
fetch_recent_messages() {
return this.get<Message[]>(`/api/v1/messages`);
}
fetch_message_children(messageId: string) {
return this.get<Message[]>(`/api/v1/messages/${messageId}/children`);
}
fetch_conversation(messageId: string) {
return this.get(`/api/v1/messages/${messageId}/conversation`);
}
}
const oasstApiClient = new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY);
export { oasstApiClient };
+11
View File
@@ -0,0 +1,11 @@
import { JWT } from "next-auth/jwt";
import { OasstApiClient } from "src/lib/oasst_api_client";
import { getBackendUserCore } from "src/lib/users";
import { BackendUserCore } from "src/types/Users";
export const createApiClientFromUser = (user: BackendUserCore) =>
new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY, user);
export const createApiClient = async (token: JWT) => createApiClientFromUser(await getBackendUserCore(token.sub));
export const userlessApiClient = new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY);
+27 -1
View File
@@ -1,6 +1,32 @@
import parser from "accept-language-parser";
import type { NextApiRequest } from "next";
import { i18n } from "src/../next-i18next.config";
import prisma from "src/lib/prismadb";
import type { BackendUserCore } from "src/types/Users";
const LOCALE_SET = new Set(i18n.locales);
/**
* Returns the most appropriate user language using the following priority:
*
* 1. The `NEXT_LOCALE` cookie which is set by the client side and will be in
* the set of supported locales.
* 2. The `accept-language` header if it contains a supported locale as set by
* the i18n module.
* 3. "en" as a final fallback.
*/
const getUserLanguage = (req: NextApiRequest): string => {
const cookieLanguage = req.cookies["NEXT_LOCALE"];
if (cookieLanguage) {
return cookieLanguage;
}
const headerLanguages = parser.parse(req.headers["accept-language"]);
if (headerLanguages.length > 0 && LOCALE_SET.has(headerLanguages[0].code)) {
return headerLanguages[0].code;
}
return "en";
};
/**
* Returns a `BackendUserCore` that can be used for interacting with the Backend service.
*
@@ -35,4 +61,4 @@ const getBackendUserCore = async (id: string) => {
} as BackendUserCore;
};
export { getBackendUserCore };
export { getBackendUserCore, getUserLanguage };