summaryrefslogtreecommitdiff
path: root/packages/backend/src/remote/activitypub/kernel
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
commit0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch)
tree40874799472fa07416f17b50a398ac33b7771905 /packages/backend/src/remote/activitypub/kernel
parentupdate deps (diff)
downloadmisskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz
misskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2
misskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip
refactoring
Resolve #7779
Diffstat (limited to 'packages/backend/src/remote/activitypub/kernel')
-rw-r--r--packages/backend/src/remote/activitypub/kernel/accept/follow.ts29
-rw-r--r--packages/backend/src/remote/activitypub/kernel/accept/index.ts24
-rw-r--r--packages/backend/src/remote/activitypub/kernel/add/index.ts23
-rw-r--r--packages/backend/src/remote/activitypub/kernel/announce/index.ts19
-rw-r--r--packages/backend/src/remote/activitypub/kernel/announce/note.ts67
-rw-r--r--packages/backend/src/remote/activitypub/kernel/block/index.ts22
-rw-r--r--packages/backend/src/remote/activitypub/kernel/create/index.ts43
-rw-r--r--packages/backend/src/remote/activitypub/kernel/create/note.ts44
-rw-r--r--packages/backend/src/remote/activitypub/kernel/delete/actor.ts26
-rw-r--r--packages/backend/src/remote/activitypub/kernel/delete/index.ts49
-rw-r--r--packages/backend/src/remote/activitypub/kernel/delete/note.ts41
-rw-r--r--packages/backend/src/remote/activitypub/kernel/flag/index.ts30
-rw-r--r--packages/backend/src/remote/activitypub/kernel/follow.ts20
-rw-r--r--packages/backend/src/remote/activitypub/kernel/index.ts71
-rw-r--r--packages/backend/src/remote/activitypub/kernel/like.ts21
-rw-r--r--packages/backend/src/remote/activitypub/kernel/move/index.ts0
-rw-r--r--packages/backend/src/remote/activitypub/kernel/read.ts27
-rw-r--r--packages/backend/src/remote/activitypub/kernel/reject/follow.ts29
-rw-r--r--packages/backend/src/remote/activitypub/kernel/reject/index.ts24
-rw-r--r--packages/backend/src/remote/activitypub/kernel/remove/index.ts23
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/announce.ts17
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/block.ts20
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/follow.ts41
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/index.ts34
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/like.ts21
-rw-r--r--packages/backend/src/remote/activitypub/kernel/update/index.ts34
26 files changed, 799 insertions, 0 deletions
diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts
new file mode 100644
index 0000000000..1afb733ab5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts
@@ -0,0 +1,29 @@
+import { IRemoteUser } from '@/models/entities/user';
+import accept from '@/services/following/requests/accept';
+import { IFollow } from '../../type';
+import DbResolver from '../../db-resolver';
+import { relayAccepted } from '@/services/relay';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+ // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
+
+ const dbResolver = new DbResolver();
+ const follower = await dbResolver.getUserFromApId(activity.actor);
+
+ if (follower == null) {
+ return `skip: follower not found`;
+ }
+
+ if (follower.host != null) {
+ return `skip: follower is not a local user`;
+ }
+
+ // relay
+ const match = activity.id?.match(/follow-relay\/(\w+)/);
+ if (match) {
+ return await relayAccepted(match[1]);
+ }
+
+ await accept(actor, follower);
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts
new file mode 100644
index 0000000000..5c6f81b2e3
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/accept/index.ts
@@ -0,0 +1,24 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import acceptFollow from './follow';
+import { IAccept, isFollow, getApType } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
+ const uri = activity.id || activity;
+
+ logger.info(`Accept: ${uri}`);
+
+ const resolver = new Resolver();
+
+ const object = await resolver.resolve(activity.object).catch(e => {
+ logger.error(`Resolution failed: ${e}`);
+ throw e;
+ });
+
+ if (isFollow(object)) return await acceptFollow(actor, object);
+
+ return `skip: Unknown Accept type: ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts
new file mode 100644
index 0000000000..b33be0cc85
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/add/index.ts
@@ -0,0 +1,23 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IAdd } from '../../type';
+import { resolveNote } from '../../models/note';
+import { addPinned } from '@/services/i/pin';
+
+export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
+ if ('actor' in activity && actor.uri !== activity.actor) {
+ throw new Error('invalid actor');
+ }
+
+ if (activity.target == null) {
+ throw new Error('target is null');
+ }
+
+ if (activity.target === actor.featured) {
+ const note = await resolveNote(activity.object);
+ if (note == null) throw new Error('note not found');
+ await addPinned(actor, note.id);
+ return;
+ }
+
+ throw new Error(`unknown target: ${activity.target}`);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts
new file mode 100644
index 0000000000..581357e577
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/announce/index.ts
@@ -0,0 +1,19 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import announceNote from './note';
+import { IAnnounce, getApId } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
+ const uri = getApId(activity);
+
+ logger.info(`Announce: ${uri}`);
+
+ const resolver = new Resolver();
+
+ const targetUri = getApId(activity.object);
+
+ announceNote(resolver, actor, activity, targetUri);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts
new file mode 100644
index 0000000000..5230867f24
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts
@@ -0,0 +1,67 @@
+import Resolver from '../../resolver';
+import post from '@/services/note/create';
+import { IRemoteUser } from '@/models/entities/user';
+import { IAnnounce, getApId } from '../../type';
+import { fetchNote, resolveNote } from '../../models/note';
+import { apLogger } from '../../logger';
+import { extractDbHost } from '@/misc/convert-host';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { getApLock } from '@/misc/app-lock';
+import { parseAudience } from '../../audience';
+import { StatusError } from '@/misc/fetch';
+
+const logger = apLogger;
+
+/**
+ * アナウンスアクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
+ const uri = getApId(activity);
+
+ // アナウンサーが凍結されていたらスキップ
+ if (actor.isSuspended) {
+ return;
+ }
+
+ // アナウンス先をブロックしてたら中断
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) return;
+
+ const unlock = await getApLock(uri);
+
+ try {
+ // 既に同じURIを持つものが登録されていないかチェック
+ const exist = await fetchNote(uri);
+ if (exist) {
+ return;
+ }
+
+ // Announce対象をresolve
+ let renote;
+ try {
+ renote = await resolveNote(targetUri);
+ } catch (e) {
+ // 対象が4xxならスキップ
+ if (e instanceof StatusError && e.isClientError) {
+ logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
+ return;
+ }
+ logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
+ throw e;
+ }
+
+ logger.info(`Creating the (Re)Note: ${uri}`);
+
+ const activityAudience = await parseAudience(actor, activity.to, activity.cc);
+
+ await post(actor, {
+ createdAt: activity.published ? new Date(activity.published) : null,
+ renote,
+ visibility: activityAudience.visibility,
+ visibleUsers: activityAudience.visibleUsers,
+ uri
+ });
+ } finally {
+ unlock();
+ }
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts
new file mode 100644
index 0000000000..4fd1e07b9b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts
@@ -0,0 +1,22 @@
+import { IBlock } from '../../type';
+import block from '@/services/blocking/create';
+import { IRemoteUser } from '@/models/entities/user';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
+ // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
+
+ const dbResolver = new DbResolver();
+ const blockee = await dbResolver.getUserFromApId(activity.object);
+
+ if (blockee == null) {
+ return `skip: blockee not found`;
+ }
+
+ if (blockee.host != null) {
+ return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
+ }
+
+ await block(actor, blockee);
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/create/index.ts b/packages/backend/src/remote/activitypub/kernel/create/index.ts
new file mode 100644
index 0000000000..ce039a363b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/create/index.ts
@@ -0,0 +1,43 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import createNote from './note';
+import { ICreate, getApId, isPost, getApType } from '../../type';
+import { apLogger } from '../../logger';
+import { toArray, concat, unique } from '@/prelude/array';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
+ const uri = getApId(activity);
+
+ logger.info(`Create: ${uri}`);
+
+ // copy audiences between activity <=> object.
+ if (typeof activity.object === 'object') {
+ const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
+ const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)]));
+
+ activity.to = to;
+ activity.cc = cc;
+ activity.object.to = to;
+ activity.object.cc = cc;
+ }
+
+ // If there is no attributedTo, use Activity actor.
+ if (typeof activity.object === 'object' && !activity.object.attributedTo) {
+ activity.object.attributedTo = activity.actor;
+ }
+
+ const resolver = new Resolver();
+
+ const object = await resolver.resolve(activity.object).catch(e => {
+ logger.error(`Resolution failed: ${e}`);
+ throw e;
+ });
+
+ if (isPost(object)) {
+ createNote(resolver, actor, object, false, activity);
+ } else {
+ logger.warn(`Unknown type: ${getApType(object)}`);
+ }
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts
new file mode 100644
index 0000000000..14e311e4cd
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts
@@ -0,0 +1,44 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import { createNote, fetchNote } from '../../models/note';
+import { getApId, IObject, ICreate } from '../../type';
+import { getApLock } from '@/misc/app-lock';
+import { extractDbHost } from '@/misc/convert-host';
+import { StatusError } from '@/misc/fetch';
+
+/**
+ * 投稿作成アクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
+ const uri = getApId(note);
+
+ if (typeof note === 'object') {
+ if (actor.uri !== note.attributedTo) {
+ return `skip: actor.uri !== note.attributedTo`;
+ }
+
+ if (typeof note.id === 'string') {
+ if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
+ return `skip: host in actor.uri !== note.id`;
+ }
+ }
+ }
+
+ const unlock = await getApLock(uri);
+
+ try {
+ const exist = await fetchNote(note);
+ if (exist) return 'skip: note exists';
+
+ await createNote(note, resolver, silent);
+ return 'ok';
+ } catch (e) {
+ if (e instanceof StatusError && e.isClientError) {
+ return `skip ${e.statusCode}`;
+ } else {
+ throw e;
+ }
+ } finally {
+ unlock();
+ }
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts
new file mode 100644
index 0000000000..502f8d5ab5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts
@@ -0,0 +1,26 @@
+import { apLogger } from '../../logger';
+import { createDeleteAccountJob } from '@/queue';
+import { IRemoteUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+const logger = apLogger;
+
+export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
+ logger.info(`Deleting the Actor: ${uri}`);
+
+ if (actor.uri !== uri) {
+ return `skip: delete actor ${actor.uri} !== ${uri}`;
+ }
+
+ if (actor.isDeleted) {
+ logger.info(`skip: already deleted`);
+ }
+
+ const job = await createDeleteAccountJob(actor);
+
+ await Users.update(actor.id, {
+ isDeleted: true,
+ });
+
+ return `ok: queued ${job.name} ${job.id}`;
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts
new file mode 100644
index 0000000000..86a452de76
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts
@@ -0,0 +1,49 @@
+import deleteNote from './note';
+import { IRemoteUser } from '@/models/entities/user';
+import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type';
+import { toSingle } from '@/prelude/array';
+import { deleteActor } from './actor';
+
+/**
+ * 削除アクティビティを捌きます
+ */
+export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
+ if ('actor' in activity && actor.uri !== activity.actor) {
+ throw new Error('invalid actor');
+ }
+
+ // 削除対象objectのtype
+ let formarType: string | undefined;
+
+ if (typeof activity.object === 'string') {
+ // typeが不明だけど、どうせ消えてるのでremote resolveしない
+ formarType = undefined;
+ } else {
+ const object = activity.object as IObject;
+ if (isTombstone(object)) {
+ formarType = toSingle(object.formerType);
+ } else {
+ formarType = toSingle(object.type);
+ }
+ }
+
+ const uri = getApId(activity.object);
+
+ // type不明でもactorとobjectが同じならばそれはPersonに違いない
+ if (!formarType && actor.uri === uri) {
+ formarType = 'Person';
+ }
+
+ // それでもなかったらおそらくNote
+ if (!formarType) {
+ formarType = 'Note';
+ }
+
+ if (validPost.includes(formarType)) {
+ return await deleteNote(actor, uri);
+ } else if (validActor.includes(formarType)) {
+ return await deleteActor(actor, uri);
+ } else {
+ return `Unknown type ${formarType}`;
+ }
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
new file mode 100644
index 0000000000..3875a33d13
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
@@ -0,0 +1,41 @@
+import { IRemoteUser } from '@/models/entities/user';
+import deleteNode from '@/services/note/delete';
+import { apLogger } from '../../logger';
+import DbResolver from '../../db-resolver';
+import { getApLock } from '@/misc/app-lock';
+import { deleteMessage } from '@/services/messages/delete';
+
+const logger = apLogger;
+
+export default async function(actor: IRemoteUser, uri: string): Promise<string> {
+ logger.info(`Deleting the Note: ${uri}`);
+
+ const unlock = await getApLock(uri);
+
+ try {
+ const dbResolver = new DbResolver();
+ const note = await dbResolver.getNoteFromApId(uri);
+
+ if (note == null) {
+ const message = await dbResolver.getMessageFromApId(uri);
+ if (message == null) return 'message not found';
+
+ if (message.userId !== actor.id) {
+ return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
+ }
+
+ await deleteMessage(message);
+
+ return 'ok: message deleted';
+ }
+
+ if (note.userId !== actor.id) {
+ return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
+ }
+
+ await deleteNode(actor, note);
+ return 'ok: note deleted';
+ } finally {
+ unlock();
+ }
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts
new file mode 100644
index 0000000000..7abfd694cd
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts
@@ -0,0 +1,30 @@
+import { IRemoteUser } from '@/models/entities/user';
+import config from '@/config/index';
+import { IFlag, getApIds } from '../../type';
+import { AbuseUserReports, Users } from '@/models/index';
+import { In } from 'typeorm';
+import { genId } from '@/misc/gen-id';
+
+export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
+ // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
+ // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
+ const uris = getApIds(activity.object);
+
+ const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop());
+ const users = await Users.find({
+ id: In(userIds)
+ });
+ if (users.length < 1) return `skip`;
+
+ await AbuseUserReports.insert({
+ id: genId(),
+ createdAt: new Date(),
+ targetUserId: users[0].id,
+ targetUserHost: users[0].host,
+ reporterId: actor.id,
+ reporterHost: actor.host,
+ comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`
+ });
+
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts
new file mode 100644
index 0000000000..3183207afa
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/follow.ts
@@ -0,0 +1,20 @@
+import { IRemoteUser } from '@/models/entities/user';
+import follow from '@/services/following/create';
+import { IFollow } from '../type';
+import DbResolver from '../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+ const dbResolver = new DbResolver();
+ const followee = await dbResolver.getUserFromApId(activity.object);
+
+ if (followee == null) {
+ return `skip: followee not found`;
+ }
+
+ if (followee.host != null) {
+ return `skip: フォローしようとしているユーザーはローカルユーザーではありません`;
+ }
+
+ await follow(actor, followee, activity.id);
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts
new file mode 100644
index 0000000000..20df28eec6
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/index.ts
@@ -0,0 +1,71 @@
+import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type';
+import { IRemoteUser } from '@/models/entities/user';
+import create from './create/index';
+import performDeleteActivity from './delete/index';
+import performUpdateActivity from './update/index';
+import { performReadActivity } from './read';
+import follow from './follow';
+import undo from './undo/index';
+import like from './like';
+import announce from './announce/index';
+import accept from './accept/index';
+import reject from './reject/index';
+import add from './add/index';
+import remove from './remove/index';
+import block from './block/index';
+import flag from './flag/index';
+import { apLogger } from '../logger';
+import Resolver from '../resolver';
+import { toArray } from '@/prelude/array';
+
+export async function performActivity(actor: IRemoteUser, activity: IObject) {
+ if (isCollectionOrOrderedCollection(activity)) {
+ const resolver = new Resolver();
+ for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
+ const act = await resolver.resolve(item);
+ try {
+ await performOneActivity(actor, act);
+ } catch (e) {
+ apLogger.error(e);
+ }
+ }
+ } else {
+ await performOneActivity(actor, activity);
+ }
+}
+
+async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> {
+ if (actor.isSuspended) return;
+
+ if (isCreate(activity)) {
+ await create(actor, activity);
+ } else if (isDelete(activity)) {
+ await performDeleteActivity(actor, activity);
+ } else if (isUpdate(activity)) {
+ await performUpdateActivity(actor, activity);
+ } else if (isRead(activity)) {
+ await performReadActivity(actor, activity);
+ } else if (isFollow(activity)) {
+ await follow(actor, activity);
+ } else if (isAccept(activity)) {
+ await accept(actor, activity);
+ } else if (isReject(activity)) {
+ await reject(actor, activity);
+ } else if (isAdd(activity)) {
+ await add(actor, activity).catch(err => apLogger.error(err));
+ } else if (isRemove(activity)) {
+ await remove(actor, activity).catch(err => apLogger.error(err));
+ } else if (isAnnounce(activity)) {
+ await announce(actor, activity);
+ } else if (isLike(activity)) {
+ await like(actor, activity);
+ } else if (isUndo(activity)) {
+ await undo(actor, activity);
+ } else if (isBlock(activity)) {
+ await block(actor, activity);
+ } else if (isFlag(activity)) {
+ await flag(actor, activity);
+ } else {
+ apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
+ }
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts
new file mode 100644
index 0000000000..58d5aefefc
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/like.ts
@@ -0,0 +1,21 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { ILike, getApId } from '../type';
+import create from '@/services/note/reaction/create';
+import { fetchNote, extractEmojis } from '../models/note';
+
+export default async (actor: IRemoteUser, activity: ILike) => {
+ const targetUri = getApId(activity.object);
+
+ const note = await fetchNote(targetUri);
+ if (!note) return `skip: target note not found ${targetUri}`;
+
+ await extractEmojis(activity.tag || [], actor.host).catch(() => null);
+
+ return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
+ if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
+ return 'skip: already reacted';
+ } else {
+ throw e;
+ }
+ }).then(() => 'ok');
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/move/index.ts
diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts
new file mode 100644
index 0000000000..11a1731869
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/read.ts
@@ -0,0 +1,27 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IRead, getApId } from '../type';
+import { isSelfHost, extractDbHost } from '@/misc/convert-host';
+import { MessagingMessages } from '@/models/index';
+import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message';
+
+export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
+ const id = await getApId(activity.object);
+
+ if (!isSelfHost(extractDbHost(id))) {
+ return `skip: Read to foreign host (${id})`;
+ }
+
+ const messageId = id.split('/').pop();
+
+ const message = await MessagingMessages.findOne(messageId);
+ if (message == null) {
+ return `skip: message not found`;
+ }
+
+ if (actor.id != message.recipientId) {
+ return `skip: actor is not a message recipient`;
+ }
+
+ await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
+ return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
new file mode 100644
index 0000000000..356547440f
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
@@ -0,0 +1,29 @@
+import { IRemoteUser } from '@/models/entities/user';
+import reject from '@/services/following/requests/reject';
+import { IFollow } from '../../type';
+import DbResolver from '../../db-resolver';
+import { relayRejected } from '@/services/relay';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+ // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
+
+ const dbResolver = new DbResolver();
+ const follower = await dbResolver.getUserFromApId(activity.actor);
+
+ if (follower == null) {
+ return `skip: follower not found`;
+ }
+
+ if (follower.host != null) {
+ return `skip: follower is not a local user`;
+ }
+
+ // relay
+ const match = activity.id?.match(/follow-relay\/(\w+)/);
+ if (match) {
+ return await relayRejected(match[1]);
+ }
+
+ await reject(actor, follower);
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts
new file mode 100644
index 0000000000..d0de9c329b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/reject/index.ts
@@ -0,0 +1,24 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import rejectFollow from './follow';
+import { IReject, isFollow, getApType } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
+ const uri = activity.id || activity;
+
+ logger.info(`Reject: ${uri}`);
+
+ const resolver = new Resolver();
+
+ const object = await resolver.resolve(activity.object).catch(e => {
+ logger.error(`Resolution failed: ${e}`);
+ throw e;
+ });
+
+ if (isFollow(object)) return await rejectFollow(actor, object);
+
+ return `skip: Unknown Reject type: ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts
new file mode 100644
index 0000000000..d59953e653
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/remove/index.ts
@@ -0,0 +1,23 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IRemove } from '../../type';
+import { resolveNote } from '../../models/note';
+import { removePinned } from '@/services/i/pin';
+
+export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
+ if ('actor' in activity && actor.uri !== activity.actor) {
+ throw new Error('invalid actor');
+ }
+
+ if (activity.target == null) {
+ throw new Error('target is null');
+ }
+
+ if (activity.target === actor.featured) {
+ const note = await resolveNote(activity.object);
+ if (note == null) throw new Error('note not found');
+ await removePinned(actor, note.id);
+ return;
+ }
+
+ throw new Error(`unknown target: ${activity.target}`);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts
new file mode 100644
index 0000000000..7bb9d7fcad
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts
@@ -0,0 +1,17 @@
+import { Notes } from '@/models/index';
+import { IRemoteUser } from '@/models/entities/user';
+import { IAnnounce, getApId } from '../../type';
+import deleteNote from '@/services/note/delete';
+
+export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
+ const uri = getApId(activity);
+
+ const note = await Notes.findOne({
+ uri
+ });
+
+ if (!note) return 'skip: no such Announce';
+
+ await deleteNote(actor, note);
+ return 'ok: deleted';
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts
new file mode 100644
index 0000000000..61940486be
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/block.ts
@@ -0,0 +1,20 @@
+import { IBlock } from '../../type';
+import unblock from '@/services/blocking/delete';
+import { IRemoteUser } from '@/models/entities/user';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
+ const dbResolver = new DbResolver();
+ const blockee = await dbResolver.getUserFromApId(activity.object);
+
+ if (blockee == null) {
+ return `skip: blockee not found`;
+ }
+
+ if (blockee.host != null) {
+ return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`;
+ }
+
+ await unblock(actor, blockee);
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts
new file mode 100644
index 0000000000..d85c7e4a71
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts
@@ -0,0 +1,41 @@
+import unfollow from '@/services/following/delete';
+import cancelRequest from '@/services/following/requests/cancel';
+import { IFollow } from '../../type';
+import { IRemoteUser } from '@/models/entities/user';
+import { FollowRequests, Followings } from '@/models/index';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+ const dbResolver = new DbResolver();
+
+ const followee = await dbResolver.getUserFromApId(activity.object);
+ if (followee == null) {
+ return `skip: followee not found`;
+ }
+
+ if (followee.host != null) {
+ return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`;
+ }
+
+ const req = await FollowRequests.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
+ });
+
+ const following = await Followings.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
+ });
+
+ if (req) {
+ await cancelRequest(followee, actor);
+ return `ok: follow request canceled`;
+ }
+
+ if (following) {
+ await unfollow(actor, followee);
+ return `ok: unfollowed`;
+ }
+
+ return `skip: リクエストもフォローもされていない`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
new file mode 100644
index 0000000000..14b1add152
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
@@ -0,0 +1,34 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
+import unfollow from './follow';
+import unblock from './block';
+import undoLike from './like';
+import { undoAnnounce } from './announce';
+import Resolver from '../../resolver';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
+ if ('actor' in activity && actor.uri !== activity.actor) {
+ throw new Error('invalid actor');
+ }
+
+ const uri = activity.id || activity;
+
+ logger.info(`Undo: ${uri}`);
+
+ const resolver = new Resolver();
+
+ const object = await resolver.resolve(activity.object).catch(e => {
+ logger.error(`Resolution failed: ${e}`);
+ throw e;
+ });
+
+ if (isFollow(object)) return await unfollow(actor, object);
+ if (isBlock(object)) return await unblock(actor, object);
+ if (isLike(object)) return await undoLike(actor, object);
+ if (isAnnounce(object)) return await undoAnnounce(actor, object);
+
+ return `skip: unknown object type ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts
new file mode 100644
index 0000000000..107d3053e3
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts
@@ -0,0 +1,21 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { ILike, getApId } from '../../type';
+import deleteReaction from '@/services/note/reaction/delete';
+import { fetchNote } from '../../models/note';
+
+/**
+ * Process Undo.Like activity
+ */
+export default async (actor: IRemoteUser, activity: ILike) => {
+ const targetUri = getApId(activity.object);
+
+ const note = await fetchNote(targetUri);
+ if (!note) return `skip: target note not found ${targetUri}`;
+
+ await deleteReaction(actor, note).catch(e => {
+ if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return;
+ throw e;
+ });
+
+ return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts
new file mode 100644
index 0000000000..52bfc5002e
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts
@@ -0,0 +1,34 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { getApType, IUpdate, isActor } from '../../type';
+import { apLogger } from '../../logger';
+import { updateQuestion } from '../../models/question';
+import Resolver from '../../resolver';
+import { updatePerson } from '../../models/person';
+
+/**
+ * Updateアクティビティを捌きます
+ */
+export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => {
+ if ('actor' in activity && actor.uri !== activity.actor) {
+ return `skip: invalid actor`;
+ }
+
+ apLogger.debug('Update');
+
+ const resolver = new Resolver();
+
+ const object = await resolver.resolve(activity.object).catch(e => {
+ apLogger.error(`Resolution failed: ${e}`);
+ throw e;
+ });
+
+ if (isActor(object)) {
+ await updatePerson(actor.uri!, resolver, object);
+ return `ok: Person updated`;
+ } else if (getApType(object) === 'Question') {
+ await updateQuestion(object).catch(e => console.log(e));
+ return `ok: Question updated`;
+ } else {
+ return `skip: Unknown type: ${getApType(object)}`;
+ }
+};