diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
| commit | 0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch) | |
| tree | 40874799472fa07416f17b50a398ac33b7771905 /packages/backend/src/remote/activitypub/kernel | |
| parent | update deps (diff) | |
| download | misskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz misskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2 misskey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip | |
refactoring
Resolve #7779
Diffstat (limited to 'packages/backend/src/remote/activitypub/kernel')
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)}`; + } +}; |