Skip to content

Commit

Permalink
spec(ActivityPub): 個別ユーザーのinboxに届いた限定公開のPostはそのユーザーに閲覧権限があると見なす (Miss…
Browse files Browse the repository at this point in the history
  • Loading branch information
u1-liquid authored and noellabo committed Aug 8, 2024
1 parent cc53bc3 commit f046bf1
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 23 deletions.
39 changes: 39 additions & 0 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,45 @@ export class NoteCreateService implements OnApplicationShutdown {
return note.renote != null;
}

public async appendNoteVisibleUser(user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
isCat: MiUser['isCat'];
}, note: MiNote, additionalUserId: MiLocalUser['id']) {
if (note.visibility !== 'specified') return;
if (note.visibleUserIds.includes(additionalUserId)) return;

const additionalUser = await this.usersRepository.findOneByOrFail({ id: additionalUserId, host: IsNull() });

// ノートのvisibleUserIdsを更新
await this.notesRepository.update(note.id, {
visibleUserIds: () => `array_append("visibleUserIds", '${additionalUser.id}')`,
});

// 新しい対象ユーザーにだけ処理が行われるようにする
note.visibleUserIds = [additionalUser.id];

// FanoutTimelineに追加
this.pushToTl(note, user);

// 未読として追加
this.noteReadService.insertNoteUnread(additionalUser.id, note, {
isSpecified: true,
isMentioned: false,
});

// ストリームに流す
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
this.globalEventService.publishNotesStream(noteObj);

// 通知を作成
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
await this.createMentionedEvents([additionalUser], note, nm);
nm.notify();
}

@bindThis
private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/PollService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class PollService {
if (note.localOnly) return;

const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) throw new Error('note not found');
if (user == null) throw new Error('user not found');

if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ export class QueueService {
}

@bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
public inbox(user: ThinUser | null, activity: IActivity, signature: httpSignature.IParsedSignature) {
const data = {
user: user ?? undefined,
activity: activity,
signature,
};
Expand Down
52 changes: 39 additions & 13 deletions packages/backend/src/core/activitypub/ApInboxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
Expand Down Expand Up @@ -88,15 +88,15 @@ export class ApInboxService {
}

@bindThis
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
public async performActivity(actor: MiRemoteUser, activity: IObject, additionalTo?: MiLocalUser['id']): Promise<string | void> {
let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
try {
results.push([getApId(item), await this.performOneActivity(actor, act)]);
results.push([getApId(item), await this.performOneActivity(actor, act, additionalTo)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
Expand All @@ -111,7 +111,7 @@ export class ApInboxService {
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
}
} else {
result = await this.performOneActivity(actor, activity);
result = await this.performOneActivity(actor, activity, additionalTo);
}

// ついでにリモートユーザーの情報が古かったら更新しておく
Expand All @@ -126,15 +126,15 @@ export class ApInboxService {
}

@bindThis
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalTo?: MiLocalUser['id']): Promise<string | void> {
if (actor.isSuspended) return;

if (isCreate(activity)) {
return await this.create(actor, activity);
return await this.create(actor, activity, additionalTo);
} else if (isDelete(activity)) {
return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
return await this.update(actor, activity);
return await this.update(actor, activity, additionalTo);
} else if (isFollow(activity)) {
return await this.follow(actor, activity);
} else if (isAccept(activity)) {
Expand Down Expand Up @@ -362,7 +362,7 @@ export class ApInboxService {
}

@bindThis
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
private async create(actor: MiRemoteUser, activity: ICreate, additionalTo?: MiLocalUser['id']): Promise<string | void> {
const uri = getApId(activity);

this.logger.info(`Create: ${uri}`);
Expand Down Expand Up @@ -395,14 +395,14 @@ export class ApInboxService {
});

if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
await this.createNote(resolver, actor, object, false, activity, additionalTo);
} else {
return `Unknown type: ${getApType(object)}`;
}
}

@bindThis
private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate, additionalTo?: MiLocalUser['id']): Promise<string> {
const uri = getApId(note);

if (typeof note === 'object') {
Expand All @@ -421,9 +421,14 @@ export class ApInboxService {

try {
const exist = await this.apNoteService.fetchNote(note);
if (exist) return 'skip: note exists';
if (additionalTo && exist && !await this.noteEntityService.isVisibleForMe(exist, additionalTo)) {
await this.noteCreateService.appendNoteVisibleUser(actor, exist, additionalTo);
return 'ok: note visible user appended';
} else if (exist) {
return 'skip: note exists';
}

await this.apNoteService.createNote(note, resolver, silent);
await this.apNoteService.createNote(note, resolver, silent, additionalTo);
return 'ok';
} catch (err) {
if (err instanceof StatusError && !err.isRetryable) {
Expand Down Expand Up @@ -750,7 +755,7 @@ export class ApInboxService {
}

@bindThis
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
private async update(actor: MiRemoteUser, activity: IUpdate, additionalTo?: MiLocalUser['id']): Promise<string> {
if (actor.uri !== activity.actor) {
return 'skip: invalid actor';
}
Expand All @@ -770,6 +775,27 @@ export class ApInboxService {
} else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
return 'ok: Question updated';
} else if (additionalTo && isPost(object)) {
const uri = getApId(object);
const unlock = await this.appLockService.getApLock(uri);

try {
const exist = await this.apNoteService.fetchNote(object);
if (exist && !await this.noteEntityService.isVisibleForMe(exist, additionalTo)) {
await this.noteCreateService.appendNoteVisibleUser(actor, exist, additionalTo);
return 'ok: note visible user appended';
} else {
return 'skip: nothing to do';
}
} catch (err) {
if (err instanceof StatusError && !err.isRetryable) {
return `skip ${err.statusCode}`;
} else {
throw err;
}
} finally {
unlock();
}
} else {
return `skip: Unknown type: ${getApType(object)}`;
}
Expand Down
18 changes: 14 additions & 4 deletions packages/backend/src/core/activitypub/models/ApNoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
*/

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { In, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
import type { UsersRepository, PollsRepository, EmojisRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js';
Expand Down Expand Up @@ -46,6 +46,9 @@ export class ApNoteService {
@Inject(DI.config)
private config: Config,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,

Expand Down Expand Up @@ -113,7 +116,7 @@ export class ApNoteService {
* Noteを作成します。
*/
@bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> {
public async createNote(value: string | IObject, resolver?: Resolver, silent = false, additionalTo?: MiLocalUser['id']): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();

Expand Down Expand Up @@ -198,6 +201,13 @@ export class ApNoteService {
let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers;

if (additionalTo) {
const additionalUser = await this.usersRepository.findOneBy({ id: additionalTo, host: IsNull() });
if (additionalUser && !visibleUsers.some(x => x.id === additionalUser.id)) {
visibleUsers.push(additionalUser);
}
}

// Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) {
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class InboxProcessorService {

// アクティビティを処理
try {
const result = await this.apInboxService.performActivity(authUser.user, activity);
const result = await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id);
if (result && !result.startsWith('ok')) {
this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
return result;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/queue/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type DeliverJobData = {
};

export type InboxJobData = {
user?: ThinUser;
activity: IActivity;
signature: httpSignature.IParsedSignature;
};
Expand Down
22 changes: 19 additions & 3 deletions packages/backend/src/server/ActivityPubServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export class ActivityPubServerService {
}

@bindThis
private inbox(request: FastifyRequest, reply: FastifyReply) {
private async inbox(request: FastifyRequest, reply: FastifyReply) {
const userId = (request.params as { user: string; } | undefined)?.user;
let signature;

try {
Expand Down Expand Up @@ -162,8 +163,23 @@ export class ActivityPubServerService {
}
}

this.queueService.inbox(request.body as IActivity, signature);
const user = userId ? await this.usersRepository.findOneBy({
id: userId,
host: IsNull(),
}) : null;

if (userId && user == null) {
reply.code(404);
return;
}

const activity = request.body as IActivity;
if (!activity.type || !signature.keyId) {
reply.code(400);
return;
}

await this.queueService.inbox(user, activity, signature);
reply.code(202);
}

Expand Down Expand Up @@ -547,7 +563,7 @@ export class ActivityPubServerService {
//#region Routing
// inbox (limit: 64kb)
fastify.post('/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
fastify.post('/users/:user/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
fastify.post<{ Params: { user: string; }; }>('/users/:user/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));

// note
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
Expand Down

0 comments on commit f046bf1

Please sign in to comment.