spec(ActivityPub): 個別ユーザーのinboxに届いた限定公開のPostはそのユーザーに閲覧権限があると見なす (MisskeyIO#361)
This commit is contained in:
parent
fd0966bc8c
commit
83bf53c600
|
@ -715,6 +715,46 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.index(note);
|
this.index(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
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
|
@bindThis
|
||||||
private isQuote(note: Option): note is Option & { renote: MiNote } {
|
private isQuote(note: Option): note is Option & { renote: MiNote } {
|
||||||
// sync with misc/is-quote.ts
|
// sync with misc/is-quote.ts
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class PollService {
|
||||||
if (note.localOnly) return;
|
if (note.localOnly) return;
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
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)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||||
|
|
|
@ -137,8 +137,9 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
public inbox(user: ThinUser | null, activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||||
const data = {
|
const data = {
|
||||||
|
user: user ?? undefined,
|
||||||
activity: activity,
|
activity: activity,
|
||||||
signature,
|
signature,
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.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';
|
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';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
|
@ -87,13 +87,13 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
public async performActivity(actor: MiRemoteUser, activity: IObject, additionalTo?: MiLocalUser['id']): Promise<void> {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||||
const act = await resolver.resolve(item);
|
const act = await resolver.resolve(item);
|
||||||
try {
|
try {
|
||||||
await this.performOneActivity(actor, act);
|
await this.performOneActivity(actor, act, additionalTo);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
@ -103,7 +103,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.performOneActivity(actor, activity);
|
await this.performOneActivity(actor, activity, additionalTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ついでにリモートユーザーの情報が古かったら更新しておく
|
// ついでにリモートユーザーの情報が古かったら更新しておく
|
||||||
|
@ -117,15 +117,15 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalTo?: MiLocalUser['id']): Promise<void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
if (isCreate(activity)) {
|
||||||
await this.create(actor, activity);
|
await this.create(actor, activity, additionalTo);
|
||||||
} else if (isDelete(activity)) {
|
} else if (isDelete(activity)) {
|
||||||
await this.delete(actor, activity);
|
await this.delete(actor, activity);
|
||||||
} else if (isUpdate(activity)) {
|
} else if (isUpdate(activity)) {
|
||||||
await this.update(actor, activity);
|
await this.update(actor, activity, additionalTo);
|
||||||
} else if (isFollow(activity)) {
|
} else if (isFollow(activity)) {
|
||||||
await this.follow(actor, activity);
|
await this.follow(actor, activity);
|
||||||
} else if (isAccept(activity)) {
|
} else if (isAccept(activity)) {
|
||||||
|
@ -346,7 +346,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
|
private async create(actor: MiRemoteUser, activity: ICreate, additionalTo?: MiLocalUser['id']): Promise<void> {
|
||||||
const uri = getApId(activity);
|
const uri = getApId(activity);
|
||||||
|
|
||||||
this.logger.info(`Create: ${uri}`);
|
this.logger.info(`Create: ${uri}`);
|
||||||
|
@ -375,14 +375,14 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(object)) {
|
if (isPost(object)) {
|
||||||
await this.createNote(resolver, actor, object, false, activity);
|
await this.createNote(resolver, actor, object, false, activity, additionalTo);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
const uri = getApId(note);
|
||||||
|
|
||||||
if (typeof note === 'object') {
|
if (typeof note === 'object') {
|
||||||
|
@ -401,9 +401,14 @@ export class ApInboxService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exist = await this.apNoteService.fetchNote(note);
|
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';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && !err.isRetryable) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
|
@ -731,7 +736,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
@ -751,6 +756,27 @@ export class ApInboxService {
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => this.logger.error(`err: failed to update question: ${err}`, { error: err }));
|
await this.apQuestionService.updateQuestion(object, resolver).catch(err => this.logger.error(`err: failed to update question: ${err}`, { error: err }));
|
||||||
return 'ok: Question updated';
|
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 {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
|
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { In } from 'typeorm';
|
import { In, IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { 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 type { MiNote } from '@/models/Note.js';
|
||||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
|
@ -46,6 +46,9 @@ export class ApNoteService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
@ -113,7 +116,7 @@ export class ApNoteService {
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
|
@ -163,6 +166,13 @@ export class ApNoteService {
|
||||||
let visibility = noteAudience.visibility;
|
let visibility = noteAudience.visibility;
|
||||||
const visibleUsers = noteAudience.visibleUsers;
|
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) が指定されてなかった場合
|
// Audience (to, cc) が指定されてなかった場合
|
||||||
if (visibility === 'specified' && visibleUsers.length === 0) {
|
if (visibility === 'specified' && visibleUsers.length === 0) {
|
||||||
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
||||||
|
|
|
@ -180,7 +180,7 @@ export class InboxProcessorService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// アクティビティを処理
|
// アクティビティを処理
|
||||||
await this.apInboxService.performActivity(authUser.user, activity);
|
await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ export type DeliverJobData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InboxJobData = {
|
export type InboxJobData = {
|
||||||
|
user?: ThinUser;
|
||||||
activity: IActivity;
|
activity: IActivity;
|
||||||
signature: httpSignature.IParsedSignature;
|
signature: httpSignature.IParsedSignature;
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,7 +100,8 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -162,14 +163,23 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
const activity = request.body as IActivity;
|
||||||
if (!activity.type || !signature.keyId) {
|
if (!activity.type || !signature.keyId) {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queueService.inbox(activity, signature);
|
await this.queueService.inbox(user, activity, signature);
|
||||||
|
|
||||||
reply.code(202);
|
reply.code(202);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,7 +563,7 @@ export class ActivityPubServerService {
|
||||||
//#region Routing
|
//#region Routing
|
||||||
// inbox (limit: 64kb)
|
// inbox (limit: 64kb)
|
||||||
fastify.post('/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
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
|
// note
|
||||||
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||||
|
|
Loading…
Reference in a new issue