Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ MongoDB にはセッション自動削除用の TTL インデックスが必要
特に `ACTIVITYPUB_DOMAIN` は Mastodon など外部からアクセスされる公開ドメインを
設定してください。未設定の場合はリクエストされたホスト名が利用されます。 以前の
`TENANT_ID` 変数は廃止され、ドメイン名そのものがテナント ID として扱われます。
リレーサーバーの設定は UI から追加・削除でき、データベースに保存されます。
登録したリレーとは `Follow` を送っておくことで投稿が inbox に届きます。
`getEnv(c)` で取得した環境変数を `fetchJson` や `deliverActivityPubObject`
へ渡すことで マルチテナント環境でも正しいドメインが利用されます
FASP の設定はデータベースで管理され、管理UI `/admin/fasps` から登録や capability
切替を行います。`getEnv(c)` で取得した環境変数を `fetchJson` や
`deliverActivityPubObject` へ渡すことでマルチテナント環境でも正しいドメインが
利用されます

### 初期設定

Expand Down Expand Up @@ -130,7 +130,7 @@ ActivityPub 形式の一覧が必要な場合は、`/ap/users/:username/follower
できます。外部との連携には ActivityPub の `/users/:username/outbox` を利用して
ください。

- `GET /api/posts` – 公開タイムラインを取得(登録済みリレーからの投稿も含む)
- `GET /api/posts` – 公開タイムラインを取得
- `GET /api/posts?timeline=following&actor=URI` –
フォロー中アクターの投稿のみ取得
- `POST /api/posts` – 投稿を作成 (`{ "author": "user", "content": "hello" }`)
Expand Down Expand Up @@ -175,17 +175,17 @@ ActivityPub の `Video` オブジェクトを利用して動画を投稿でき
WebSocket では Base64 文字列を、HTTP POST では multipart/form-data の
`thumbnail` パートに画像ファイルを送信してください。

## リレー API
## FASP連携とService Actor配信

ほかのインスタンスと連携するためのリレーサーバーを管理します。
takos host は Fediverse Auxiliary Service Provider (FASP) と連携するための API
を `/fasp` に提供し、登録・プロバイダ情報取得・capability 切替および Discovery
機能
(`data_sharing`、`trends`、`account_search`)を利用できます。設定はデータベースで
管理され、管理UI `/admin/fasps` から操作します。

- `GET /api/relays` – 登録済みリレー一覧を取得
- `POST /api/relays` – `{ "inboxUrl": "https://relay.example/inbox" }`
を送信して追加
- `DELETE /api/relays/:id` – リレーを削除

各インスタンスのリストは `relays` コレクションに基づきます。 takos host
のデフォルトリレーは自動登録されますが、一覧には表示されません。
また ActivityPub の Service Actor を `/actor` で公開し、外部サービスはこの Actor
を フォローすることで投稿やアナウンスを受信できます。詳細は
[docs/FASP.md](docs/FASP.md) を参照してください。

## アカウント管理 API

Expand Down
199 changes: 131 additions & 68 deletions app/api/DB/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import HostNote from "../models/takos_host/note.ts";
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 HostFaspConfig from "../models/takos_host/fasp_config.ts";
import HostFaspRegistration from "../models/takos_host/fasp_registration.ts";
import HostFaspEventSubscription from "../models/takos_host/fasp_event_subscription.ts";
import HostFaspBackfillRequest from "../models/takos_host/fasp_backfill_request.ts";
import SystemKey from "../models/takos/system_key.ts";
import HostRelay from "../models/takos_host/relay.ts";
import ServiceActorKey from "../models/takos/service_actor_key.ts";
import HostRemoteActor from "../models/takos_host/remote_actor.ts";
import HostSession from "../models/takos_host/session.ts";
import HostFcmToken from "../models/takos_host/fcm_token.ts";
Expand All @@ -22,7 +26,14 @@ 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,
FaspBackfillRequestDoc,
FaspConfigDoc,
FaspEventSubscriptionDoc,
FaspRegistrationDoc,
SessionDoc,
} from "../../shared/types.ts";
import type { SortOrder } from "mongoose";
import type { Db } from "mongodb";
import { connectDatabase } from "../../shared/db.ts";
Expand All @@ -39,13 +50,8 @@ 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 useLocalObjects() {
return false;
}

private async searchObjects(
Expand All @@ -59,7 +65,7 @@ export class MongoDBHost implements DB {
const conds: Record<string, unknown>[] = [
{ ...baseFilter, tenant_id: this.tenantId },
];
if (await this.useLocalObjects()) {
if (this.useLocalObjects()) {
conds.push({ ...baseFilter, tenant_id: this.rootDomain });
}
const exec = async (
Expand Down Expand Up @@ -532,27 +538,6 @@ 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);
}

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 },
);
}

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

async addFollowerByName(username: string, follower: string) {
await HostAccount.updateOne({
userName: username,
Expand Down Expand Up @@ -778,43 +763,6 @@ 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,
}));
}

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;
}

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 };
}

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

async findRemoteActorByUrl(url: string) {
return await HostRemoteActor.findOne({ actorUrl: url }).lean();
}
Expand Down Expand Up @@ -857,6 +805,121 @@ export class MongoDBHost implements DB {
await SystemKey.create({ domain, privateKey, publicKey });
}

async findServiceActorKey(domain: string) {
return await ServiceActorKey.findOne({ domain }).lean<
{ domain: string; privateKey: string; publicKey: string } | null
>();
}

async saveServiceActorKey(
domain: string,
privateKey: string,
publicKey: string,
) {
await ServiceActorKey.create({ domain, privateKey, publicKey });
}

/** FASP設定取得 */
async findFaspConfig(): Promise<FaspConfigDoc | null> {
return await HostFaspConfig.findOne({ tenant_id: this.tenantId }).lean<
FaspConfigDoc | null
>();
}

/** FASP設定保存 */
async saveFaspConfig(config: FaspConfigDoc) {
await HostFaspConfig.updateOne({ tenant_id: this.tenantId }, {
$set: config,
}, { upsert: true });
}

/** FASP設定削除 */
async deleteFaspConfig() {
await HostFaspConfig.deleteOne({ tenant_id: this.tenantId });
}

/** FASP 登録情報作成 */
async createFaspRegistration(reg: FaspRegistrationDoc) {
const doc = new HostFaspRegistration({ ...reg, tenant_id: this.tenantId });
await doc.save();
return doc.toObject();
}

/** 登録済みFASP一覧取得 */
async listFaspRegistrations(): Promise<FaspRegistrationDoc[]> {
const docs = await HostFaspRegistration.find({ tenant_id: this.tenantId })
.lean<FaspRegistrationDoc[]>();
return docs;
}

/** FASP 登録の承認 */
async approveFaspRegistration(id: string) {
await HostFaspRegistration.updateOne({
fasp_id: id,
tenant_id: this.tenantId,
}, { $set: { approved: true } });
}

/** server_id から登録情報検索 */
async findFaspRegistrationByServerId(serverId: string) {
return await HostFaspRegistration.findOne({
server_id: serverId,
tenant_id: this.tenantId,
}).lean<FaspRegistrationDoc | null>();
}

/** capability 選択の更新 */
async updateFaspCapability(
serverId: string,
capability: { id: string; version: string },
enabled: boolean,
) {
const update = enabled
? { $addToSet: { capabilities: capability } }
: { $pull: { capabilities: capability } };
await HostFaspRegistration.updateOne({
server_id: serverId,
tenant_id: this.tenantId,
}, update);
}

async createFaspEventSubscription(sub: FaspEventSubscriptionDoc) {
const doc = new HostFaspEventSubscription({
...sub,
tenant_id: this.tenantId,
});
await doc.save();
return doc.toObject();
}

async deleteFaspEventSubscription(id: string) {
await HostFaspEventSubscription.deleteOne({
_id: id,
tenant_id: this.tenantId,
});
}

async createFaspBackfillRequest(req: FaspBackfillRequestDoc) {
const doc = new HostFaspBackfillRequest({
...req,
tenant_id: this.tenantId,
});
await doc.save();
return doc.toObject();
}

async findFaspBackfillRequestById(id: string) {
return await HostFaspBackfillRequest.findOne({
_id: id,
tenant_id: this.tenantId,
}).lean<FaspBackfillRequestDoc | null>();
}

async findFaspRegistration() {
return await HostFaspRegistration.findOne({ tenant_id: this.tenantId })
.lean<FaspRegistrationDoc | null>();
}

async registerFcmToken(token: string, userName: string) {
if (this.env["DB_MODE"] === "host") {
await HostFcmToken.updateOne(
Expand Down
Loading