First Commit

This commit is contained in:
2026-01-19 19:51:25 +09:00
commit 5a16b0dbbb
28 changed files with 1321 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import type { generateAuthURIOptions } from "1.6.11/types/auth";
import { v4 as uuidv4 } from "uuid";
/** ユーザー認可によるトークン取得のURIを生成します。 */
export default function generateAuthURI(options: generateAuthURIOptions) {
const session = options.session ?? uuidv4();
const uri = new URL("/api/auth", options.origin);
uri.searchParams.set("session", session);
uri.searchParams.set("client", options.name);
uri.searchParams.set("scope", options.scope.join(","));
if (options.icon)
uri.searchParams.set("icon", options.icon.toString());
if (options.about)
uri.searchParams.set("about", options.about);
if (options.callback) {
if (options.callback.insertSession)
options.callback.url.searchParams.set(options.callback.insertSession, session);
uri.searchParams.set("icon", options.callback.toString());
}
return {
/** 生成されたURI */
uri: uri,
/** session */
session: session,
};
}
+2
View File
@@ -0,0 +1,2 @@
import { ApiMap } from "1.6.11/types/api/map";
export default ApiMap;
+18
View File
@@ -0,0 +1,18 @@
import ServerInfo from "1.6.11/types/api/serverinfo-api";
import Users from "1.6.11/types/api/users";
import UsersFollow from "1.6.11/types/api/users/follow";
import Me from "1.6.11/types/api/me";
import MeNotification from "1.6.11/types/api/me/notification";
import MeNotificationRead from "1.6.11/types/api/me/notification/read";
import MeSettings from "1.6.11/types/api/me/settings";
import Ueuse from "1.6.11/types/api/ueuse";
export type ApiMap =
& ServerInfo
& Me
& MeNotification
& MeNotificationRead
& MeSettings
& Users
& UsersFollow
& Ueuse;
+8
View File
@@ -0,0 +1,8 @@
import { UserResponse } from "1.6.11/types/api/users";
export default interface Me {
"me/": {
body: never;
response: UserResponse;
};
};
@@ -0,0 +1,58 @@
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
export default interface MeNotification {
"me/notification/": {
body?: {
/** 制限数 */
limit?: number;
/** ページ */
page?: number;
};
response: {
/** 成功かどうか */
success: true;
/** 通知 */
data: {
/** 送信元 */
from: {
/** ユーザー名(表示名) */
username: string;
/** ユーザーID */
userid: string;
/** アイコン画像URL */
user_icon: string;
/** ヘッダー画像URL */
user_header: string;
};
/** カテゴリ */
category: "system" | "favorite" |
"reply" | "reuse" |
"ueuse" | "follow" |
"mention" | "other" |
"login";
/** タイトル */
title: string;
/** 内容 */
text: string;
/**
* 送信時刻
* YYYY-MM-DD HH:MM:SS形式です。
*/
datetime: string;
/**
* 関連するuniqid
* 例えば、返信やリユーズ、メンションなどの場合に参照ユーズのuniqidが入ります。
*/
valueid?: string;
/** 既読かどうか */
is_checked: boolean;
}[];
} | InputError | AuthError | {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "notification_not_found";
}
}
};
@@ -0,0 +1,13 @@
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
import UpdateError from "1.6.11/types/modules/error/update";
export default interface MeNotificationRead {
"me/notification/read": {
body: never;
response: {
/** 成功かどうか */
success: true;
} | InputError | AuthError | UpdateError;
};
};
+62
View File
@@ -0,0 +1,62 @@
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
import UpdateError from "1.6.11/types/modules/error/update";
import { Upload1Error, Upload2Error, UploadCommonError } from "1.6.11/types/modules/error/upload";
type AtLeastOne<T> = {
[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>
}[keyof T];
type EmptyString = "";
type UsernameErrors<B> =
B extends { username: EmptyString }
? {
success: false;
error_code:
| "表示名を入力してください。(USERNAME_INPUT_PLEASE)";
}
: B extends { username: string }
? {
success: false;
error_code:
| "ユーザーネームは50文字以内で入力してください。(USERNAME_OVER_MAX_COUNT)";
}
: never;
type ProfileErrors<B> =
B extends { profile: string }
? {
success: false;
error_code:
| "プロフィールは1024文字以内で入力してください。(INPUT_OVER_MAX_COUNT)";
}
: never;
type Response<B> =
| {
/** 成功かどうか */
success: true;
}
| InputError
| AuthError
| UpdateError
| UploadCommonError
| Upload1Error
| Upload2Error
| UsernameErrors<B>
| ProfileErrors<B>;
export default interface MeSettings {
"me/settings/": {
body: AtLeastOne<{
username: string | EmptyString;
profile: string;
icon: string;
header: string;
}>;
response: Response<
MeSettings["me/settings/"]["body"]
>;
};
}
+73
View File
@@ -0,0 +1,73 @@
export default interface ServerInfo {
"serverinfo-api": {
body: never;
response: {
server_info: {
/** サーバー名 */
server_name: string;
/** サーバーアイコン画像URL */
server_icon: string;
/** サーバー説明文 */
server_description: string;
/** サーバー管理者 */
adminstor: {
/** 名前 */
name: string;
/** メールアドレス */
email: string;
};
/** 利用規約URL */
terms_url: string;
/** プライバシーポリシーURL */
privacy_policy_url: string;
/** 最大文字数 */
max_ueuse_length: number;
/** 招待制である */
invitation_code: boolean;
/** アカウント移行が許可されている */
account_migration: boolean;
/** 統計 */
usage: {
/** ユーザー数 */
users: number;
/** ユーズ数 */
ueuse: number;
};
};
/** ソフトウェア */
software: {
/**
* 名称
* 公式uwuzuである場合はuwuzuです。
* 改変uwuzuでかつその改変に名称がある場合はuwuzu以外です。
*/
name: string;
/**
* バージョン
* vは入りません。
*/
version: string;
/**
* リポジトリURL
* 公式uwuzuである場合はhttps://github.com/Daichimarukana/uwuzuです。
* 改変uwuzuでかつその改変内容が公開されている場合は各リポジトリURLです。
*/
repository: string;
};
/** お知らせ */
server_notice: {
/** タイトル */
title: string;
/** 内容 */
note: string;
/** 作成者(ユーザーID) */
editor: string;
/**
* 作成時刻
* YYYY-MM-DD HH:MM:SS形式です。
*/
datetime: string;
}[];
};
};
}
+21
View File
@@ -0,0 +1,21 @@
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
import { ueuseModule } from "1.6.11/types/modules/ueuse";
import ueuseError from "1.6.11/types/modules/error/ueuse";
export default interface Ueuse {
"ueuse/": {
body?: {
/** 制限数 */
limit?: number;
/** ページ */
page?: number;
};
response: {
/** 成功かどうか */
success: true;
/** ユーズ(LTL) */
data: ueuseModule[];
} | InputError | AuthError | ueuseError;
}
};
+23
View File
@@ -0,0 +1,23 @@
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
import UpdateError, { CouldNotComplete } from "1.6.11/types/modules/error/update";
import ToYouNotAllowed from "1.6.11/types/modules/error/follow";
import { UserDataNotFound } from "1.6.11/types/modules/error/critical";
interface Follow<T extends string> {
body: {
/** ユーザーID */
userid: T;
};
response: {
/** 成功かどうか */
success: true;
/** ユーザーID */
userid: T;
} | InputError | AuthError | UpdateError | CouldNotComplete | ToYouNotAllowed | UserDataNotFound;
}
export default interface UsersFollow {
"users/follow": Follow<string>;
"users/unfollow": Follow<string>;
}
+54
View File
@@ -0,0 +1,54 @@
import Role from "1.6.11/types/modules/role";
import InputError from "1.6.11/types/modules/error/input";
import AuthError from "1.6.11/types/modules/error/auth";
import { UserDataNotFound } from "1.6.11/types/modules/error/critical";
export type UserResponse = {
/** 成功かどうか */
success: true;
/** ユーザー名(表示名) */
username: string;
/** ユーザーID */
userid: string;
/** プロフィール */
profile: string;
/** アイコン画像URL */
user_icon: string;
/** ヘッダー画像URL */
user_header: string;
/**
* 登録時刻
* YYYY-MM-DD HH:MM:SS形式です。
*/
registered_date: string;
/** フォロー */
followee: string[];
/** フォロー数 */
followee_cnt: number;
/** フォロワー */
follower: string[];
/** フォロワー数 */
follower_cnt: number;
/** ユーズ数 */
ueuse_cnt: number;
/** Botである */
isBot: boolean;
/** 管理者である */
isAdmin: boolean;
/** ロール */
role: Role[];
/** オンラインステータス */
online_status: "Online" | "Away" | "Offline" | null;
/** 言語 */
language: "ja-JP";
} | InputError | AuthError | UserDataNotFound;
export default interface Users {
"users/": {
body: {
/** ユーザーID */
userid: string;
};
response: UserResponse;
};
};
+38
View File
@@ -0,0 +1,38 @@
export type scope =
"read:me" | "write:me" |
"read:ueuse" | "write:ueuse" |
"read:users" |
"write:follow" |
"write:favorite" |
"read:notifications" | "write:notifications" |
"read:bookmark" | "write:bookmark";
export interface generateAuthURIOptions {
/** uwuzuサーバーのorigin */
origin: string;
/** * アプリケーションの名称 */
name: string;
/** 要求する権限 */
scope: scope[];
/** アプリケーションの説明 */
about?: string;
/** アプリケーションのアイコンURL */
icon?: URL;
/** 許可された際に移動するURL */
callback?: {
/** コールバックURL */
url: URL;
/**
* sessionをURLパラメータに挿入します。
* 指定することでcallback.urlにcallback.insertSessionという名前でsessionを挿入します。
* 指定しない場合は挿入しません。
*/
insertSession?: string;
};
/**
* sessionを明示的に指定します。
* sessionはサーバー全体で一意になるようにしてください。
* 指定しない場合はUUID v4を使用します。
*/
session?: string;
}
+10
View File
@@ -0,0 +1,10 @@
export default interface AuthError {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code:
"this_account_has_been_frozen" |
"token_invalid" |
"not_allow_scope" |
"token_invalid_scope";
}
@@ -0,0 +1,6 @@
export type UserDataNotFound = {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "critical_error_userdata_not_found";
};
+6
View File
@@ -0,0 +1,6 @@
export default interface ToYouNotAllowed {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "you_cant_it_to_yourself";
}
+6
View File
@@ -0,0 +1,6 @@
export default interface InputError {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "input_not_found";
}
+6
View File
@@ -0,0 +1,6 @@
export default interface ueuseError {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "ueuse_not_found";
}
+13
View File
@@ -0,0 +1,13 @@
export default interface UpdateError {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "update_failed";
}
export interface CouldNotComplete {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code: "could_not_complete";
}
+44
View File
@@ -0,0 +1,44 @@
export interface UploadCommonError {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code:
"ERROR" |
"使用できない画像形式です。(FILE_UPLOAD_DEKINAKATTA)";
}
export interface Upload1Error {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code:
"アップロード失敗!(1)エラーコード:FILE_DEKASUGUI_PHP_INI_KAKUNIN" |
"アップロード失敗!(1)エラーコード:FILE_DEKASUGUI_HTML_KAKUNIN" |
"アップロード失敗!(1)エラーコード:FILE_SUKOSHIDAKE_UPLOAD" |
"アップロード失敗!(1)エラーコード:FILE_UPLOAD_DEKINAKATTA" |
"アップロード失敗!(1)エラーコード:TMP_FOLDER_NAI" |
"アップロード失敗!(1)エラーコード:FILE_KAKIKOMI_SIPPAI" |
"アップロード失敗!(1)エラーコード:PHPINFO()_KAKUNIN" |
"アップロード失敗!(1)エラーコード:TMP_FILE_NAI" |
"アップロード失敗!(1)エラーコード:SAVE_FOLDER_KAKIKOMI_KENNAI" |
"アップロード失敗!(1)エラーコード:MOVE_UPLOAD_FILE_SIPPAI" |
"アップロード失敗!(1)エラーコード: S3ERROR";
}
export interface Upload2Error {
/** 成功かどうか */
success: false;
/** エラーコード */
error_code:
"アップロード失敗!(2)エラーコード:FILE_DEKASUGUI_PHP_INI_KAKUNIN" |
"アップロード失敗!(2)エラーコード:FILE_DEKASUGUI_HTML_KAKUNIN" |
"アップロード失敗!(2)エラーコード:FILE_SUKOSHIDAKE_UPLOAD" |
"アップロード失敗!(2)エラーコード:FILE_UPLOAD_DEKINAKATTA" |
"アップロード失敗!(2)エラーコード:TMP_FOLDER_NAI" |
"アップロード失敗!(2)エラーコード:FILE_KAKIKOMI_SIPPAI" |
"アップロード失敗!(2)エラーコード:PHPINFO()_KAKUNIN" |
"アップロード失敗!(2)エラーコード:TMP_FILE_NAI" |
"アップロード失敗!(2)エラーコード:SAVE_FOLDER_KAKIKOMI_KENNAI" |
"アップロード失敗!(2)エラーコード:MOVE_UPLOAD_FILE_SIPPAI" |
"アップロード失敗!(2)エラーコード: S3ERROR";
}
+10
View File
@@ -0,0 +1,10 @@
export default interface Role {
/** ロール名(表示名) */
name: string;
/** HEXカラー(#なし) */
color: string;
/** エフェクト */
effect: "none" | "shine" | "rainbow";
/** ロールID */
id: string;
}
+204
View File
@@ -0,0 +1,204 @@
type Abi = {
/**
* 追記
* 追記がない場合は空文字列です。
*/
abi: string;
/** 追記時刻 */
abidatetime: string;
} | {
/**
* 追記
* 追記が入力できないため常に空文字列です。
*/
abi: "";
/**
* 追記時刻
* 追記が入力できないため常に0000-00-00 00:00:00です。
*/
abidatetime: "0000-00-00 00:00:00";
}
type Media =
| {
/**
* 画像1URL
* 画像1がないため常に空文字列です。
*/
photo1?: undefined;
/**
* 画像2URL
* 画像2がないため常に空文字列です。
* 画像2は画像1が入力されていることの内包条件です。
*/
photo2?: undefined;
/**
* 画像3URL
* 画像3がないため常に空文字列です。
* 画像3は画像2が入力されていることの内包条件です。
*/
photo3?: undefined;
/**
* 画像4URL
* 画像4がないため常に空文字列です。
* 画像4は画像3が入力されていることの内包条件です。
*/
photo4?: undefined;
/**
* 動画URL
* 動画がないため常に空文字列です。
*/
video1?: undefined;
} | {
/** 画像1URL */
photo1: string;
/**
* 画像2URL
* 画像2がない場合は空文字列です。
* 画像2は画像1が入力されていることの内包条件です。
*/
photo2?: string;
/**
* 画像3URL
* 画像3がない場合は空文字列です。
* 画像3は画像2が入力されていることの内包条件です。
*/
photo3?: string;
/**
* 画像4URL
* 画像4がない場合は空文字列です。
* 画像4は画像3が入力されていることの内包条件です。
*/
photo4?: string;
/**
* 動画URL
* 動画がないため常に空文字列です。
*/
video1?: undefined;
} | {
/** 動画URL */
video1: string;
/**
* 画像1URL
* 画像1がないため常に空文字列です。
*/
photo1?: undefined;
/**
* 画像2URL
* 画像2がないため常に空文字列です。
* 画像2は画像1が入力されていることの内包条件です。
*/
photo2?: undefined;
/**
* 画像3URL
* 画像3がないため常に空文字列です。
* 画像3は画像1が入力されていることの内包条件です。
*/
photo3?: undefined;
/**
* 画像4URL
* 画像4がないため常に空文字列です。
* 画像4は画像1が入力されていることの内包条件です。
*/
photo4?: undefined;
};
interface ueuseBase {
/** ユニークID */
uniqid: string;
/** 送信者 */
account: {
/** ユーザー名 */
username: string;
/** ユーザーID */
userid: string;
/** ユーザーアイコン画像URL */
user_icon: string;
/** ユーザーヘッダー画像URL */
user_header: string;
/** Botフラグ(自主設定)かどうか */
is_bot: boolean;
};
/** いいねしたユーザーID */
favorite: string[];
/** いいね数 */
favorite_cnt: number;
/** 返信数 */
reply_cnt: number;
/** リユーズ+引用数 */
reuse_cnt: number;
/** 送信時刻 */
datetime: string;
/** NSFW(自主設定)かどうか */
nsfw: boolean;
}
interface NormalUeuse extends ueuseBase {
/** 本文 */
text: string;
/**
* 返信先ID
* 返信でないため空文字列です。
*/
replyid: "";
/**
* リユーズ/引用元ID
* リユーズ/引用でないため空文字列です。
*/
reuseid: "";
}
interface ReplyUeuse extends ueuseBase {
/** 本文 */
text: string;
/** 返信先ID */
replyid: string;
/**
* リユーズ/引用元ID
* リユーズ/引用でないため空文字列です。
*/
reuseid: "";
}
interface Reuse extends ueuseBase {
/**
* 本文
* リユーズであるため常に空文字列です。
*/
text: "";
/**
* 返信先ID
* 返信でないため空文字列です。
*/
replyid: "";
/** リユーズ/引用元ID */
reuseid: string;
/**
* 追記
* リユーズであるため常に空文字列です。
*/
abi: "";
/**
* 追記時刻
* リユーズであるため常に0000-00-00 00:00:00です。
*/
abidatetime: "0000-00-00 00:00:00";
}
interface QuoteReuse extends ueuseBase {
/** 本文 */
text: string;
/**
* 返信先ID
* 返信でないため空文字列です。
*/
replyid: "";
/** 引用元ID */
reuseid: string;
}
export type ueuseModule =
| (NormalUeuse & Abi & Media)
| (ReplyUeuse & Abi & Media)
| (QuoteReuse & Abi & Media)
| Reuse;
+114
View File
@@ -0,0 +1,114 @@
import uwuzuError from "@/lib/error";
import uwuzuFetch from "@/lib/fetch";
interface sdkOptions {
/** uwuzuサーバーのorigin */
origin: string;
/**
* 通信に失敗した際の再試行回数です。
* 全て失敗した場合はエラーを発生します。
* 指定しない場合は5回が設定されます。
*/
retry?: number;
/**
* 通信に失敗した際に動作する再試行間の待機時間(ミリ秒)です。
* 再試行毎に2倍にされます。
* 指定しない場合は500msが設定されます。
*/
waiting?: number;
}
/* 必須かどうかの推論 */
type BodyArgs<M, E extends keyof M> =
M[E] extends { body: never } ? [] :
M[E] extends { body: infer B } ? [body: B] :
M[E] extends { body?: infer B } ? [body?: B] :
[];
export default class uwuzu<
M extends { [K in keyof M]: { body?: any; response: any } }
> {
readonly origin: string;
readonly retry: number;
readonly waiting: number;
private _token: string | null = null;
constructor(options: sdkOptions) {
this.origin = options.origin;
this.retry = options.retry ?? 5;
this.waiting = options.waiting ?? 500;
if (this.retry < 1) throw new uwuzuError("Invalid retry count.");
if (this.waiting < 1) throw new uwuzuError("Invalid base waiting time.");
if (options.origin !== new URL(options.origin).origin)
throw new uwuzuError("Invalid origin.");
}
/** APIトークン */
get token(): string | null {
return this._token;
}
set token(token: string) {
if (token.length !== 64) throw new uwuzuError("Invalid token.");
this._token = token;
}
/** APIリクエスト */
// 型あり
public async request<E extends keyof M>(
endpoint: E,
...args: BodyArgs<M, E>
): Promise<M[E]["response"]>;
// 型なし
public async request(
endpoint: string,
body?: any
): Promise<any>;
public async request(
endpoint: string,
...args: any[]
): Promise<any> {
let bodyParsed: any = args[0] ?? {};
if (typeof bodyParsed === "object") {
bodyParsed = {
...bodyParsed,
token: this._token,
};
bodyParsed = JSON.stringify(bodyParsed);
}
const req = await uwuzuFetch(
this.origin,
this.retry,
this.waiting,
"endpoint",
endpoint as string,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
body: bodyParsed,
}
);
let res = await req.json();
if (res["0"] !== undefined && res.success === true) {
res.success = undefined;
const data = Object.values(res).filter(Boolean);
return {
success: true,
data,
};
}
return res;
}
}
+7
View File
@@ -0,0 +1,7 @@
/** better-uwuzu-sdkのエラー */
export default class uwuzuError extends Error {
constructor(message: string) {
super(message);
this.name = "uwuzuError";
}
}
+68
View File
@@ -0,0 +1,68 @@
import uwuzuError from "@/lib/error";
export function generateURL(
origin: string,
endpoint: string,
) {
const uri = new URL(`/api/${endpoint}`, origin);
return uri.toString();
}
export default async function uwuzuFetch(
origin: string,
retryCount: number,
waitingTime: number,
inputType: "endpoint" | "other",
input: string,
init?: RequestInit,
) {
const waiting = (ms: number) =>
new Promise<void>(resolve => setTimeout(resolve, ms));
if (inputType === "endpoint")
input = generateURL(origin, input);
let lastError;
for (let i = 0; i < retryCount; i++) {
try {
if (init?.signal?.aborted) {
throw new DOMException("Aborted", "AbortError");
}
const req = await fetch(input, init);
if (
Math.floor(req.status / 200) === 1 ||
Math.floor(req.status / 300) === 1
) {
return req;
}
if (Math.floor(req.status / 400) === 1)
throw new uwuzuError(`Client error: HTTP${req.status} - ${req.statusText}`);
lastError = new Error(`Request failed: HTTP${req.status} - ${req.statusText}`);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
throw err;
}
if (
err instanceof uwuzuError &&
err.name === "Client error" &&
err.message.startsWith("HTTP4")
)
throw err;
lastError = err;
}
if (i < retryCount - 1) {
const waitTime = waitingTime * 2 ** i;
await waiting(waitTime);
}
}
throw lastError ?? new Error("Unknown Error");
}