Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8df6093
FASP登録時の通信履歴を保存
tako0614 Aug 7, 2025
60242d7
std モジュールの import パスを絶対 URL に修正
tako0614 Aug 7, 2025
b0ac5ff
base64モジュールのAPI変更に追従
tako0614 Aug 7, 2025
54b1a6f
FASP登録ルートの環境変数取得と鍵生成の型を修正
tako0614 Aug 7, 2025
fdfdae5
Service Actor のマウントを修正
tako0614 Aug 7, 2025
0b5cd69
feat: FASP 管理UIコンポーネントを追加
tako0614 Aug 7, 2025
605bf8f
FASP接続用の管理UIを追加
tako0614 Aug 7, 2025
b325fa2
FASP 管理UIを簡素化し公開鍵入力を不要化
tako0614 Aug 7, 2025
430ba4a
管理UIからFASPサーバーを追加できるように
tako0614 Aug 7, 2025
7b45c4a
Nodeinfo で FASP ベースURLを常に設定
tako0614 Aug 7, 2025
099eee5
FASP応答署名ヘルパーを追加
tako0614 Aug 7, 2025
c154bc5
バックフィル要求の処理とアナウンス送信を実装
tako0614 Aug 7, 2025
11b3a7f
FASP応答検証の強化
tako0614 Aug 7, 2025
106c4fa
FASP登録のベースURL検証を強化
tako0614 Aug 7, 2025
b8e328d
fix: FASPデータ共有ルートの設定ファイル読み込みパスを修正
tako0614 Aug 7, 2025
012a918
FASP 管理 API を /api 配下に戻す
tako0614 Aug 7, 2025
6517720
FASPの管理エンドポイントを削除
tako0614 Aug 7, 2025
12b2047
feat: FASP 設定 API と UI を追加
tako0614 Aug 7, 2025
60a0099
管理APIにFASP手動登録を復活
tako0614 Aug 7, 2025
f7757bd
FASP 設定エンドポイントをAPI配下へ移動
tako0614 Aug 7, 2025
f2f46a9
FASP登録をBaseURLのみで実行できるように変更
tako0614 Aug 7, 2025
75c2e72
feat: FASP provider_info 応答を実装
tako0614 Aug 8, 2025
a727e3c
FASPルートがフロントエンドに吸い込まれる問題を修正
tako0614 Aug 8, 2025
af30de7
FASPルーティングを/api配下へ戻しクライアント表示を復旧
tako0614 Aug 8, 2025
95fb545
rootドメインでテナントアプリが表示される問題を修正
tako0614 Aug 8, 2025
ce5fe82
dev環境でフロントエンド資産をプロキシ
tako0614 Aug 8, 2025
a60dcac
dev環境でも404ページをHTMLで返すよう修正
tako0614 Aug 9, 2025
2912375
ルートドメインへのアクセスをユーザー画面へリダイレクト
tako0614 Aug 9, 2025
7e4c33b
ルートドメインでのリダイレクトを廃止
tako0614 Aug 9, 2025
eff8a9c
未マッチリクエストをユーザークライアントにリダイレクト
tako0614 Aug 9, 2025
4a13153
ルートパスへのアクセスで /user へリダイレクトしないよう修正
tako0614 Aug 9, 2025
f195b33
ルートパスのリダイレクトを復元
tako0614 Aug 9, 2025
c52452d
fix: env.mjs に __DEFINES__ が未定義で起動失敗する問題を修正
tako0614 Aug 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ WebSocket では Base64 文字列を、HTTP POST では multipart/form-data の
を送信して追加
- `DELETE /api/relays/:id` – リレーを削除

各インスタンスのリストは `relays` コレクションに基づきます。 takos host
のデフォルトリレーは自動登録されますが、一覧には表示されません。
各インスタンスのリストは `relays` コレクションに基づきます。

## アカウント管理 API

Expand Down
98 changes: 42 additions & 56 deletions app/api/DB/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import HostVideo from "../models/takos_host/video.ts";
import HostMessage from "../models/takos_host/message.ts";
import HostAttachment from "../models/takos_host/attachment.ts";
import SystemKey from "../models/takos/system_key.ts";
import HostRelay from "../models/takos_host/relay.ts";
import HostRemoteActor from "../models/takos_host/remote_actor.ts";
import HostServiceActor from "../models/takos_host/service_actor.ts";
import HostSession from "../models/takos_host/session.ts";
import HostFcmToken from "../models/takos_host/fcm_token.ts";
import FcmToken from "../models/takos/fcm_token.ts";
Expand All @@ -22,7 +22,7 @@ import OAuthClient from "../../takos_host/models/oauth_client.ts";
import HostDomain from "../../takos_host/models/domain.ts";
import mongoose from "mongoose";
import type { DB, ListOpts } from "../../shared/db.ts";
import type { AccountDoc, RelayDoc, SessionDoc } from "../../shared/types.ts";
import type { AccountDoc, SessionDoc } from "../../shared/types.ts";
import type { SortOrder } from "mongoose";
import type { Db } from "mongodb";
import { connectDatabase } from "../../shared/db.ts";
Expand All @@ -39,15 +39,6 @@ export class MongoDBHost implements DB {
return this.env["ROOT_DOMAIN"] ?? "";
}

private async useLocalObjects() {
if (!this.rootDomain) return false;
const count = await HostRelay.countDocuments({
tenant_id: this.tenantId,
host: this.rootDomain,
});
return count > 0;
}

private async searchObjects(
filter: Record<string, unknown>,
sort?: Record<string, SortOrder>,
Expand All @@ -59,9 +50,6 @@ export class MongoDBHost implements DB {
const conds: Record<string, unknown>[] = [
{ ...baseFilter, tenant_id: this.tenantId },
];
if (await this.useLocalObjects()) {
conds.push({ ...baseFilter, tenant_id: this.rootDomain });
}
const exec = async (
M:
| typeof HostNote
Expand Down Expand Up @@ -566,24 +554,16 @@ export class MongoDBHost implements DB {
return { deletedCount: 0 };
}

async listRelays() {
const docs = await HostRelay.find({ tenant_id: this.tenantId }).lean<
{ host: string }[]
>();
return docs.map((d) => d.host);
listRelays() {
return Promise.resolve<string[]>([]);
}

async addRelay(relay: string, inboxUrl?: string) {
const url = inboxUrl ?? `https://${relay}/inbox`;
await HostRelay.updateOne(
{ tenant_id: this.tenantId, host: relay },
{ $set: { inboxUrl: url }, $setOnInsert: { since: new Date() } },
{ upsert: true },
);
addRelay(_relay: string, _inboxUrl?: string) {
return Promise.resolve();
}

async removeRelay(relay: string) {
await HostRelay.deleteOne({ tenant_id: this.tenantId, host: relay });
removeRelay(_relay: string) {
return Promise.resolve();
}

async addFollowerByName(username: string, follower: string) {
Expand Down Expand Up @@ -811,41 +791,47 @@ export class MongoDBHost implements DB {
return !!res;
}

async findRelaysByHosts(hosts: string[]): Promise<RelayDoc[]> {
const docs = await HostRelay.find({ host: { $in: hosts } }).lean<
{ _id: mongoose.Types.ObjectId; host: string; inboxUrl: string }[]
>();
return docs.map((d) => ({
_id: String(d._id),
host: d.host,
inboxUrl: d.inboxUrl,
}));
findRelaysByHosts(_hosts: string[]) {
return Promise.resolve([]);
}

async findRelayByHost(host: string): Promise<RelayDoc | null> {
const doc = await HostRelay.findOne({ host }).lean<
{ _id: mongoose.Types.ObjectId; host: string; inboxUrl: string } | null
>();
return doc
? { _id: String(doc._id), host: doc.host, inboxUrl: doc.inboxUrl }
: null;
findRelayByHost(_host: string) {
return Promise.resolve(null);
}

async createRelay(
data: { host: string; inboxUrl: string },
): Promise<RelayDoc> {
const doc = new HostRelay({ host: data.host, inboxUrl: data.inboxUrl });
await doc.save();
return { _id: String(doc._id), host: doc.host, inboxUrl: doc.inboxUrl };
createRelay(data: { host: string; inboxUrl: string }) {
return Promise.resolve({
_id: "",
host: data.host,
inboxUrl: data.inboxUrl,
});
}

deleteRelayById(_id: string) {
return Promise.resolve(null);
}

async deleteRelayById(id: string): Promise<RelayDoc | null> {
const doc = await HostRelay.findByIdAndDelete(id).lean<
{ _id: mongoose.Types.ObjectId; host: string; inboxUrl: string } | null
async getServiceActorConfig() {
let doc = await HostServiceActor.findOne({}).lean<
{
enabled: boolean;
actorUrl: string;
type: string;
deliverBatchSize: number;
deliverMinIntervalMs: number;
allowInstances: string[];
denyInstances: string[];
followers: string[];
} | null
>();
return doc
? { _id: String(doc._id), host: doc.host, inboxUrl: doc.inboxUrl }
: null;
if (!doc) {
const actorUrl = this.rootDomain
? `https://${this.rootDomain}/actor`
: "";
const created = await HostServiceActor.create({ actorUrl });
doc = created.toObject();
}
return doc;
}

async findRemoteActorByUrl(url: string) {
Expand Down
57 changes: 57 additions & 0 deletions app/api/models/takos/fasp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import mongoose from "mongoose";

/** FASP登録情報と機能状態を保持するスキーマ */
const capabilitySchema = new mongoose.Schema({
identifier: { type: String, required: true },
version: { type: String, required: true },
enabled: { type: Boolean, default: false },
});

const eventSubscriptionSchema = new mongoose.Schema({
id: { type: String, required: true },
category: { type: String, required: true },
subscriptionType: { type: String, required: true },
created_at: { type: Date, default: Date.now },
});

const backfillRequestSchema = new mongoose.Schema({
id: { type: String, required: true },
category: { type: String, required: true },
maxCount: { type: Number, required: true },
status: { type: String, enum: ["pending", "completed"], default: "pending" },
created_at: { type: Date, default: Date.now },
});

const communicationLogSchema = new mongoose.Schema({
direction: { type: String, enum: ["in", "out"], required: true },
endpoint: { type: String, required: true },
payload: { type: mongoose.Schema.Types.Mixed },
created_at: { type: Date, default: Date.now },
});

const faspSchema = new mongoose.Schema({
_id: { type: String, required: true },
name: { type: String, required: true },
baseUrl: { type: String, required: true },
serverId: { type: String, required: true },
faspPublicKey: { type: String, required: true },
publicKey: { type: String, required: true },
privateKey: { type: String, required: true },
accepted: { type: Boolean, default: false },
capabilities: { type: [capabilitySchema], default: [] },
eventSubscriptions: { type: [eventSubscriptionSchema], default: [] },
backfillRequests: { type: [backfillRequestSchema], default: [] },
communications: { type: [communicationLogSchema], default: [] },
created_at: { type: Date, default: Date.now },
});

const Fasp = mongoose.models.Fasp ?? mongoose.model("Fasp", faspSchema);

export default Fasp;
export {
backfillRequestSchema,
capabilitySchema,
communicationLogSchema,
eventSubscriptionSchema,
faspSchema,
};
12 changes: 12 additions & 0 deletions app/api/models/takos_host/fasp_server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import mongoose from "mongoose";

const faspServerSchema = new mongoose.Schema({
serverId: { type: String, required: true },
publicKey: { type: String, required: true },
privateKey: { type: String, required: true },
});

const HostFaspServer = mongoose.models.HostFaspServer ??
mongoose.model("HostFaspServer", faspServerSchema);

export default HostFaspServer;
16 changes: 0 additions & 16 deletions app/api/models/takos_host/relay.ts

This file was deleted.

18 changes: 18 additions & 0 deletions app/api/models/takos_host/service_actor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import mongoose from "mongoose";

const serviceActorSchema = new mongoose.Schema({
enabled: { type: Boolean, default: true },
actorUrl: { type: String, required: true },
type: { type: String, default: "Service" },
deliverBatchSize: { type: Number, default: 20 },
deliverMinIntervalMs: { type: Number, default: 200 },
allowInstances: { type: [String], default: ["*"] },
denyInstances: { type: [String], default: [] },
followers: { type: [String], default: [] },
});

const HostServiceActor = mongoose.models.HostServiceActor ??
mongoose.model("HostServiceActor", serviceActorSchema);

export default HostServiceActor;
export { serviceActorSchema };
55 changes: 54 additions & 1 deletion app/api/routes/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Hono } from "hono";
import { jsonResponse } from "../utils/activitypub.ts";
import { getDomain, jsonResponse } from "../utils/activitypub.ts";
import authRequired from "../utils/auth.ts";
import { createDB } from "../DB/mod.ts";
import { getEnv } from "../../shared/config.ts";
Expand All @@ -8,6 +8,8 @@ import type { AccountDoc } from "../../shared/types.ts";
import { b64ToBuf } from "../../shared/buffer.ts";
import { isUrl } from "../../shared/url.ts";
import { saveFile } from "../services/file.ts";
import Fasp from "../models/takos/fasp.ts";
import { sendAnnouncement } from "../services/fasp.ts";

function formatAccount(doc: AccountDoc) {
return {
Expand Down Expand Up @@ -51,6 +53,49 @@ async function resolveAvatar(
return name.charAt(0).toUpperCase().substring(0, 2);
}

async function notifyFaspAccount(
env: Record<string, string>,
domain: string,
username: string,
eventType: "new" | "update" | "delete",
) {
const db = createDB(env);
const acc = await db.findAccountByUserName(username) as unknown as
| { extra?: { discoverable?: boolean; visibility?: string } }
| null;
if (!acc) return;
const isPublic = (acc.extra?.visibility ?? "public") === "public";
const discoverable = Boolean(acc.extra?.discoverable);
if (!isPublic || !discoverable) return;

const fasp = await Fasp.findOne({ accepted: true }) as unknown as
| {
eventSubscriptions: {
id: string;
category: string;
subscriptionType: string;
}[];
}
| null;
if (!fasp) return;
const subs = (fasp.eventSubscriptions as {
id: string;
category: string;
subscriptionType: string;
}[]).filter((s) =>
s.category === "account" && s.subscriptionType === "lifecycle"
);
const uri = `https://${domain}/users/${username}`;
for (const sub of subs) {
await sendAnnouncement(
{ subscription: { id: sub.id } },
"account",
eventType,
[uri],
);
}
}

const app = new Hono();
app.use("/accounts/*", authRequired);

Expand All @@ -63,6 +108,7 @@ app.get("/accounts", async (c) => {
});

app.post("/accounts", async (c) => {
const domain = getDomain(c);
const env = getEnv(c);
const { username, displayName, icon, privateKey, publicKey } = await c.req
.json();
Expand Down Expand Up @@ -102,6 +148,7 @@ app.post("/accounts", async (c) => {
following: [],
dms: [],
});
await notifyFaspAccount(env, domain, username.trim(), "new");
return jsonResponse(c, formatAccount(account));
});

Expand All @@ -118,6 +165,7 @@ app.get("/accounts/:id", async (c) => {
});

app.put("/accounts/:id", async (c) => {
const domain = getDomain(c);
const env = getEnv(c);
const db = createDB(env);
const id = c.req.param("id");
Expand Down Expand Up @@ -149,15 +197,20 @@ app.put("/accounts/:id", async (c) => {

const account = await db.updateAccountById(id, data);
if (!account) return jsonResponse(c, { error: "Account not found" }, 404);
await notifyFaspAccount(env, domain, account.userName, "update");
return jsonResponse(c, formatAccount(account));
});

app.delete("/accounts/:id", async (c) => {
const domain = getDomain(c);
const env = getEnv(c);
const db = createDB(env);
const id = c.req.param("id");
const acc = await db.findAccountById(id);
if (!acc) return jsonResponse(c, { error: "Account not found" }, 404);
const deleted = await db.deleteAccountById(id);
if (!deleted) return jsonResponse(c, { error: "Account not found" }, 404);
await notifyFaspAccount(env, domain, acc.userName, "delete");
return jsonResponse(c, { success: true });
});

Expand Down
19 changes: 19 additions & 0 deletions app/api/routes/fasp/account_search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Hono } from "hono";
import authRequired from "../../utils/auth.ts";
import { accountSearch } from "../../services/fasp.ts";

const app = new Hono();
app.use("/fasp/account_search/*", authRequired);

app.get("/fasp/account_search", async (c) => {
const term = c.req.query("term");
const next = c.req.query("next");
if (!term && !next) {
return c.json({ error: "term or next is required" }, 400);
}
const limit = Number(c.req.query("limit") ?? "20");
const result = await accountSearch(term, limit, next);
return c.json(result);
});

export default app;
Loading