type-safe sanitization of AP objects
Some checks failed
Publish Docker image / Build (push) Waiting to run
Lint / pnpm_install (push) Waiting to run
Lint / lint (backend) (push) Blocked by required conditions
Lint / lint (frontend) (push) Blocked by required conditions
Lint / lint (frontend-embed) (push) Blocked by required conditions
Lint / lint (frontend-shared) (push) Blocked by required conditions
Lint / lint (misskey-bubble-game) (push) Blocked by required conditions
Lint / lint (misskey-js) (push) Blocked by required conditions
Lint / lint (misskey-reversi) (push) Blocked by required conditions
Lint / lint (sw) (push) Blocked by required conditions
Lint / typecheck (backend) (push) Blocked by required conditions
Lint / typecheck (misskey-js) (push) Blocked by required conditions
Lint / typecheck (sw) (push) Blocked by required conditions
Test (backend) / unit (22.11.0) (push) Waiting to run
Test (backend) / e2e (22.11.0) (push) Waiting to run
Test (production install and build) / production (22.11.0) (push) Waiting to run
Lint / pnpm_install (pull_request) Successful in 1m20s
Publish Docker image / Build (pull_request) Successful in 4m28s
Test (production install and build) / production (22.11.0) (pull_request) Successful in 55s
Test (backend) / unit (22.11.0) (pull_request) Failing after 6m40s
Test (backend) / e2e (22.11.0) (pull_request) Failing after 9m46s
Lint / lint (backend) (pull_request) Successful in 1m53s
Lint / lint (frontend) (pull_request) Successful in 1m55s
Lint / lint (frontend-embed) (pull_request) Successful in 1m49s
Lint / lint (frontend-shared) (pull_request) Successful in 2m1s
Lint / lint (misskey-bubble-game) (pull_request) Successful in 1m58s
Lint / lint (misskey-js) (pull_request) Successful in 2m7s
Lint / lint (misskey-reversi) (pull_request) Successful in 2m11s
Lint / typecheck (backend) (pull_request) Successful in 1m55s
Lint / lint (sw) (pull_request) Successful in 2m24s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m26s
Lint / typecheck (sw) (pull_request) Successful in 1m29s

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-21 00:47:39 -06:00
parent 7a7aef71cd
commit 5c99d28c14
No known key found for this signature in database
11 changed files with 639 additions and 170 deletions

View file

@ -18,7 +18,7 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js'; import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import { URL } from 'node:url';
export type HttpRequestSendOptions = { export type HttpRequestSendOptions = {
throwErrorWhenResponseNotOk: boolean; throwErrorWhenResponseNotOk: boolean;
@ -183,6 +183,16 @@ export class HttpRequestService {
controller.abort(); controller.abort();
}, timeout); }, timeout);
const urlParsed = new URL(url);
if (urlParsed.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
if (urlParsed.port && urlParsed.port !== '443') {
throw new Error('Invalid port');
}
const res = await fetch(url, { const res = await fetch(url, {
method: args.method ?? 'GET', method: args.method ?? 'GET',
headers: { headers: {

View file

@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
@Injectable() @Injectable()
export class RemoteUserResolveService { export class RemoteUserResolveService {
@ -35,6 +36,7 @@ export class RemoteUserResolveService {
private remoteLoggerService: RemoteLoggerService, private remoteLoggerService: RemoteLoggerService,
private apDbResolverService: ApDbResolverService, private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private apResolverService: ApResolverService,
) { ) {
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
} }
@ -91,7 +93,7 @@ export class RemoteUserResolveService {
} }
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href, this.apResolverService.createResolver());
} }
// ユーザー情報が古い場合は、WebFingerからやりなおして返す // ユーザー情報が古い場合は、WebFingerからやりなおして返す

View file

@ -29,7 +29,7 @@ import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.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'; import { getApHrefNullable, getApId, getApIds, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPost, isTombstone, validActor, validPost, yumeDowncastAccept, yumeDowncastAdd, yumeDowncastAnnounce, yumeDowncastBlock, yumeDowncastCreate, yumeDowncastDelete, yumeDowncastFlag, yumeDowncastFollow, yumeDowncastLike, yumeDowncastMove, yumeDowncastReject, yumeDowncastRemove, yumeDowncastUndo, yumeDowncastUpdate } 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';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
@ -138,52 +138,92 @@ export class ApInboxService {
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> { public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
if (actor.isSuspended) return; if (actor.isSuspended) return;
if (isCreate(activity)) { const create = yumeDowncastCreate(activity);
if (create) {
mInboxReceived?.inc({ host: actor.host, type: 'create' }); mInboxReceived?.inc({ host: actor.host, type: 'create' });
return await this.create(actor, activity); return await this.create(actor, create);
} else if (isDelete(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'update' });
return await this.update(actor, activity);
} else if (isFollow(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
return await this.follow(actor, activity);
} else if (isAccept(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
return await this.accept(actor, activity);
} else if (isReject(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
return await this.reject(actor, activity);
} else if (isAdd(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'add' });
return await this.add(actor, activity);
} else if (isRemove(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
return await this.announce(actor, activity);
} else if (isLike(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'like' });
return await this.like(actor, activity);
} else if (isUndo(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
return await this.undo(actor, activity);
} else if (isBlock(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'block' });
return await this.block(actor, activity);
} else if (isFlag(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
return await this.flag(actor, activity);
} else if (isMove(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'move' });
return await this.move(actor, activity);
} else {
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
return `unrecognized activity type: ${activity.type}`;
} }
const update = yumeDowncastUpdate(activity);
if (update) {
mInboxReceived?.inc({ host: actor.host, type: 'update' });
return await this.update(actor, update);
}
const del = yumeDowncastDelete(activity);
if (del) {
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
return await this.delete(actor, del);
}
const follow = yumeDowncastFollow(activity);
if (follow) {
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
return await this.follow(actor, follow);
}
const accept = yumeDowncastAccept(activity);
if (accept) {
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
return await this.accept(actor, accept);
}
const reject = yumeDowncastReject(activity);
if (reject) {
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
return await this.reject(actor, reject);
}
const add = yumeDowncastAdd(activity);
if (add) {
mInboxReceived?.inc({ host: actor.host, type: 'add' });
return await this.add(actor, add);
}
const remove = yumeDowncastRemove(activity);
if (remove) {
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
return await this.remove(actor, remove);
}
const announce = yumeDowncastAnnounce(activity);
if (announce) {
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
return await this.announce(actor, announce);
}
const like = yumeDowncastLike(activity);
if (like) {
mInboxReceived?.inc({ host: actor.host, type: 'like' });
return await this.like(actor, like);
}
const move = yumeDowncastMove(activity);
if (move) {
mInboxReceived?.inc({ host: actor.host, type: 'move' });
return await this.move(actor, move);
}
const undo = yumeDowncastUndo(activity);
if (undo) {
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
return await this.undo(actor, undo);
}
const block = yumeDowncastBlock(activity);
if (block) {
mInboxReceived?.inc({ host: actor.host, type: 'block' });
return await this.block(actor, block);
}
const flag = yumeDowncastFlag(activity);
if (flag) {
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
return await this.flag(actor, flag);
}
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
return `unrecognized activity type: ${activity.type}`;
} }
@bindThis @bindThis
@ -234,7 +274,8 @@ export class ApInboxService {
throw err; throw err;
}); });
if (isFollow(object)) return await this.acceptFollow(actor, object); const follow = yumeDowncastFollow(object);
if (follow) return await this.acceptFollow(actor, follow);
return `skip: Unknown Accept type: ${getApType(object)}`; return `skip: Unknown Accept type: ${getApType(object)}`;
} }
@ -583,7 +624,8 @@ export class ApInboxService {
throw e; throw e;
}); });
if (isFollow(object)) return await this.rejectFollow(actor, object); const follow = yumeDowncastFollow(object);
if (follow) return await this.rejectFollow(actor, follow);
return `skip: Unknown Reject type: ${getApType(object)}`; return `skip: Unknown Reject type: ${getApType(object)}`;
} }
@ -650,11 +692,20 @@ export class ApInboxService {
}); });
// don't queue because the sender may attempt again when timeout // don't queue because the sender may attempt again when timeout
if (isFollow(object)) return await this.undoFollow(actor, object); const follow = yumeDowncastFollow(object);
if (isBlock(object)) return await this.undoBlock(actor, object); if (follow) return await this.undoFollow(actor, follow);
if (isLike(object)) return await this.undoLike(actor, object);
if (isAnnounce(object)) return await this.undoAnnounce(actor, object); const block = yumeDowncastBlock(object);
if (isAccept(object)) return await this.undoAccept(actor, object); if (block) return await this.undoBlock(actor, block);
const like = yumeDowncastLike(object);
if (like) return await this.undoLike(actor, like);
const announce = yumeDowncastAnnounce(object);
if (announce) return await this.undoAnnounce(actor, announce);
const accept = yumeDowncastAccept(object);
if (accept) return await this.undoAccept(actor, accept);
return `skip: unknown object type ${getApType(object)}`; return `skip: unknown object type ${getApType(object)}`;
} }

View file

@ -30,7 +30,7 @@ import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js'; import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js'; import { CONTEXT } from './misc/contexts.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import { markOutgoing, type IAccept, type IActivity, type IAdd, type IAnnounce, type IApDocument, type IApEmoji, type IApHashtag, type IApImage, type IApMention, type IBlock, type ICreate, type IDelete, type IFlag, type IFollow, type IKey, type ILike, type IMove, type IObject, type IPost, type IQuestion, type IReject, type IRemove, type ITombstone, type IUndo, type IUpdate } from './type.js';
@Injectable() @Injectable()
export class ApRendererService { export class ApRendererService {
@ -66,21 +66,21 @@ export class ApRendererService {
@bindThis @bindThis
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept { public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
return { return markOutgoing({
type: 'Accept', type: 'Accept',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
object, object,
}; }, undefined);
} }
@bindThis @bindThis
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd { public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
return { return markOutgoing({
type: 'Add', type: 'Add',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
target, target,
object, object,
}; }, undefined);
} }
@bindThis @bindThis
@ -103,7 +103,7 @@ export class ApRendererService {
throw new Error('renderAnnounce: cannot render non-public note'); throw new Error('renderAnnounce: cannot render non-public note');
} }
return { return markOutgoing({
id: `${this.config.url}/notes/${note.id}/activity`, id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId), actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Announce', type: 'Announce',
@ -111,7 +111,7 @@ export class ApRendererService {
to, to,
cc, cc,
object, object,
}; }, undefined);
} }
/** /**
@ -125,23 +125,23 @@ export class ApRendererService {
throw new Error('renderBlock: missing blockee uri'); throw new Error('renderBlock: missing blockee uri');
} }
return { return markOutgoing({
type: 'Block', type: 'Block',
id: `${this.config.url}/blocks/${block.id}`, id: `${this.config.url}/blocks/${block.id}`,
actor: this.userEntityService.genLocalUserUri(block.blockerId), actor: this.userEntityService.genLocalUserUri(block.blockerId),
object: block.blockee.uri, object: block.blockee.uri,
}; }, undefined);
} }
@bindThis @bindThis
public renderCreate(object: IObject, note: MiNote): ICreate { public renderCreate(object: IObject, note: MiNote): ICreate {
const activity: ICreate = { const activity: ICreate = markOutgoing({
id: `${this.config.url}/notes/${note.id}/activity`, id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId), actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create', type: 'Create',
published: this.idService.parse(note.id).date.toISOString(), published: this.idService.parse(note.id).date.toISOString(),
object, object,
}; }, undefined);
if (object.to) activity.to = object.to; if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc; if (object.cc) activity.cc = object.cc;
@ -151,28 +151,28 @@ export class ApRendererService {
@bindThis @bindThis
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete { public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
return { return markOutgoing({
type: 'Delete', type: 'Delete',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
object, object,
published: new Date().toISOString(), published: new Date().toISOString(),
}; }, undefined);
} }
@bindThis @bindThis
public renderDocument(file: MiDriveFile): IApDocument { public renderDocument(file: MiDriveFile): IApDocument {
return { return markOutgoing({
type: 'Document', type: 'Document',
mediaType: file.webpublicType ?? file.type, mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment, name: file.comment,
sensitive: file.isSensitive, sensitive: file.isSensitive,
}; }, undefined);
} }
@bindThis @bindThis
public renderEmoji(emoji: MiEmoji): IApEmoji { public renderEmoji(emoji: MiEmoji): IApEmoji {
return { return markOutgoing( {
id: `${this.config.url}/emojis/${emoji.name}`, id: `${this.config.url}/emojis/${emoji.name}`,
type: 'Emoji', type: 'Emoji',
name: `:${emoji.name}:`, name: `:${emoji.name}:`,
@ -183,28 +183,28 @@ export class ApRendererService {
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
}, },
}; }, undefined);
} }
// to anonymise reporters, the reporting actor must be a system user // to anonymise reporters, the reporting actor must be a system user
@bindThis @bindThis
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag { public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
return { return markOutgoing({
type: 'Flag', type: 'Flag',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
content, content,
object, object,
}; }, undefined);
} }
@bindThis @bindThis
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow { public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
return { return markOutgoing({
id: `${this.config.url}/activities/follow-relay/${relay.id}`, id: `${this.config.url}/activities/follow-relay/${relay.id}`,
type: 'Follow', type: 'Follow',
actor: this.userEntityService.genLocalUserUri(relayActor.id), actor: this.userEntityService.genLocalUserUri(relayActor.id),
object: 'https://www.w3.org/ns/activitystreams#Public', object: 'https://www.w3.org/ns/activitystreams#Public',
}; }, undefined);
} }
/** /**
@ -223,36 +223,36 @@ export class ApRendererService {
followee: MiPartialLocalUser | MiPartialRemoteUser, followee: MiPartialLocalUser | MiPartialRemoteUser,
requestId?: string, requestId?: string,
): IFollow { ): IFollow {
return { return markOutgoing({
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow', type: 'Follow',
actor: this.userEntityService.getUserUri(follower), actor: this.userEntityService.getUserUri(follower),
object: this.userEntityService.getUserUri(followee), object: this.userEntityService.getUserUri(followee),
}; }, undefined);
} }
@bindThis @bindThis
public renderHashtag(tag: string): IApHashtag { public renderHashtag(tag: string): IApHashtag {
return { return markOutgoing({
type: 'Hashtag', type: 'Hashtag',
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`, href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
name: `#${tag}`, name: `#${tag}`,
}; }, undefined);
} }
@bindThis @bindThis
public renderImage(file: MiDriveFile): IApImage { public renderImage(file: MiDriveFile): IApImage {
return { return markOutgoing({
type: 'Image', type: 'Image',
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getPublicUrl(file),
sensitive: file.isSensitive, sensitive: file.isSensitive,
name: file.comment, name: file.comment,
}; }, undefined);
} }
@bindThis @bindThis
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
return { return markOutgoing({
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
type: 'Key', type: 'Key',
owner: this.userEntityService.genLocalUserUri(user.id), owner: this.userEntityService.genLocalUserUri(user.id),
@ -260,21 +260,21 @@ export class ApRendererService {
type: 'spki', type: 'spki',
format: 'pem', format: 'pem',
}), }),
}; }, undefined);
} }
@bindThis @bindThis
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> { public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
const reaction = noteReaction.reaction; const reaction = noteReaction.reaction;
const object: ILike = { const object: ILike = markOutgoing({
type: 'Like', type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`, id: `${this.config.url}/likes/${noteReaction.id}`,
actor: `${this.config.url}/users/${noteReaction.userId}`, actor: `${this.config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
content: reaction, content: reaction,
_misskey_reaction: reaction, _misskey_reaction: reaction,
}; }, undefined);
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', ''); const name = reaction.replaceAll(':', '');
@ -288,11 +288,11 @@ export class ApRendererService {
@bindThis @bindThis
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention { public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
return { return markOutgoing({
type: 'Mention', type: 'Mention',
href: this.userEntityService.getUserUri(mention), href: this.userEntityService.getUserUri(mention),
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`, name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
}; }, undefined);
} }
@bindThis @bindThis
@ -302,13 +302,13 @@ export class ApRendererService {
): IMove { ): IMove {
const actor = this.userEntityService.getUserUri(src); const actor = this.userEntityService.getUserUri(src);
const target = this.userEntityService.getUserUri(dst); const target = this.userEntityService.getUserUri(dst);
return { return markOutgoing({
id: `${this.config.url}/moves/${src.id}/${dst.id}`, id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor, actor,
type: 'Move', type: 'Move',
object: actor, object: actor,
target, target,
}; }, undefined);
} }
@bindThis @bindThis
@ -422,7 +422,7 @@ export class ApRendererService {
})), })),
} as const : {}; } as const : {};
return { return markOutgoing({
id: `${this.config.url}/notes/${note.id}`, id: `${this.config.url}/notes/${note.id}`,
type: 'Note', type: 'Note',
attributedTo, attributedTo,
@ -445,7 +445,7 @@ export class ApRendererService {
sensitive: note.cw != null || files.some(file => file.isSensitive), sensitive: note.cw != null || files.some(file => file.isSensitive),
tag, tag,
...asPoll, ...asPoll,
}; }, undefined);
} }
@bindThis @bindThis
@ -529,7 +529,7 @@ export class ApRendererService {
@bindThis @bindThis
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion { public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
return { return markOutgoing({
type: 'Question', type: 'Question',
id: `${this.config.url}/questions/${note.id}`, id: `${this.config.url}/questions/${note.id}`,
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -542,78 +542,78 @@ export class ApRendererService {
totalItems: poll.votes[i], totalItems: poll.votes[i],
}, },
})), })),
}; }, 'question');
} }
@bindThis @bindThis
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject { public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
return { return markOutgoing({
type: 'Reject', type: 'Reject',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
object, object,
}; }, undefined);
} }
@bindThis @bindThis
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove { public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
return { return markOutgoing({
type: 'Remove', type: 'Remove',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
target, target,
object, object,
}; }, undefined);
} }
@bindThis @bindThis
public renderTombstone(id: string): ITombstone { public renderTombstone(id: string): ITombstone {
return { return markOutgoing({
id, id,
type: 'Tombstone', type: 'Tombstone',
}; }, undefined);
} }
@bindThis @bindThis
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo { public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
return { return markOutgoing({
type: 'Undo', type: 'Undo',
...(id ? { id } : {}), ...(id ? { id } : {}),
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
object, object,
published: new Date().toISOString(), published: new Date().toISOString(),
}; }, undefined);
} }
@bindThis @bindThis
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate { public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
return { return markOutgoing( {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Update', type: 'Update',
to: ['https://www.w3.org/ns/activitystreams#Public'], to: ['https://www.w3.org/ns/activitystreams#Public'],
object, object,
published: new Date().toISOString(), published: new Date().toISOString(),
}; }, undefined);
} }
@bindThis @bindThis
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate { public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
return { return markOutgoing({
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Create', type: 'Create',
to: [pollOwner.uri], to: [pollOwner.uri],
published: new Date().toISOString(), published: new Date().toISOString(),
object: { object: markOutgoing({
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`, id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
type: 'Note', type: 'Note',
attributedTo: this.userEntityService.genLocalUserUri(user.id), attributedTo: this.userEntityService.genLocalUserUri(user.id),
to: [pollOwner.uri], to: [pollOwner.uri],
inReplyTo: note.uri, inReplyTo: note.uri,
name: poll.choices[vote.choice], name: poll.choices[vote.choice],
}, }, undefined),
}; }, undefined);
} }
@bindThis @bindThis

View file

@ -16,11 +16,11 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { isCollectionOrOrderedCollection } from './type.js'; import { isCollectionOrOrderedCollection, yumeNormalizeObject } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js'; import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js'; import { ApRequestService } from './ApRequestService.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js'; import type { IObject, ICollection, IOrderedCollection, IUnsanitizedObject } from './type.js';
export class Resolver { export class Resolver {
private history: Set<string>; private history: Set<string>;
@ -67,7 +67,7 @@ export class Resolver {
} }
@bindThis @bindThis
public async resolve(value: string | IObject): Promise<IObject> { public async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return value; return value;
} }
@ -103,8 +103,8 @@ export class Resolver {
} }
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject ? await this.apRequestService.signedGet(value, this.user) as IUnsanitizedObject
: await this.httpRequestService.getActivityJson(value)) as IObject; : await this.httpRequestService.getActivityJson(value)) as IUnsanitizedObject;
if ( if (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?
@ -117,6 +117,13 @@ export class Resolver {
return object; return object;
} }
@bindThis
public async resolve(value: string | IObject): Promise<IObject> {
const object = await this.resolveNotNormalized(value);
return yumeNormalizeObject(object);
}
@bindThis @bindThis
private resolveLocal(url: string): Promise<IObject> { private resolveLocal(url: string): Promise<IObject> {
const parsed = this.apDbResolverService.parseUri(url); const parsed = this.apDbResolverService.parseUri(url);

View file

@ -115,10 +115,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): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
@ -356,7 +353,7 @@ export class ApNoteService {
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
return await this.createNote(createFrom, options.resolver, true); return await this.createNote(createFrom, this.apResolverService.createResolver(), true);
} finally { } finally {
unlock(); unlock();
} }

View file

@ -277,16 +277,13 @@ export class ApPersonService implements OnModuleInit {
* Personを作成します * Personを作成します
*/ */
@bindThis @bindThis
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> { public async createPerson(uri: string, resolver: Resolver): Promise<MiRemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
if (uri.startsWith(this.config.url)) { if (uri.startsWith(this.config.url)) {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
} }
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri); const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id); if (object.id == null) throw new Error('invalid object.id: ' + object.id);

View file

@ -3,20 +3,44 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { target } from "happy-dom/lib/PropertySymbol.js";
import { toASCII } from "node:punycode";
export type Obj = { [x: string]: any }; export type Obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[]; export type ApObject = IObject | string | (IObject | string)[];
export interface IObject { export interface MisskeyVendorKeys {
_misskey_summary: string;
_misskey_followedMessage: string | null;
_misskey_requireSigninToViewContents: boolean;
_misskey_makeNotesFollowersOnlyBefore: number | null;
_misskey_makeNotesHiddenBefore: number | null;
_misskey_quote: string;
_misskey_content: string;
_misskey_reaction: string;
_misskey_votes: number;
}
function extractMisskeyVendorKeys(object: IObject): Partial<MisskeyVendorKeys> {
return {
_misskey_summary: object._misskey_summary,
_misskey_followedMessage: object._misskey_followedMessage,
_misskey_requireSigninToViewContents: object._misskey_requireSigninToViewContents,
_misskey_makeNotesFollowersOnlyBefore: object._misskey_makeNotesFollowersOnlyBefore,
_misskey_makeNotesHiddenBefore: object._misskey_makeNotesHiddenBefore,
_misskey_quote: object._misskey_quote,
_misskey_content: object._misskey_content,
_misskey_reaction: object._misskey_reaction,
_misskey_votes: object._misskey_votes,
};
}
export interface IUnsanitizedObject extends Partial<MisskeyVendorKeys> {
'@context'?: string | string[] | Obj | Obj[]; '@context'?: string | string[] | Obj | Obj[];
type: string | string[]; type: string | string[];
id?: string; id?: string;
name?: string | null; name?: string | null;
summary?: string; summary?: string;
_misskey_summary?: string;
_misskey_followedMessage?: string | null;
_misskey_requireSigninToViewContents?: boolean;
_misskey_makeNotesFollowersOnlyBefore?: number | null;
_misskey_makeNotesHiddenBefore?: number | null;
published?: string; published?: string;
cc?: ApObject; cc?: ApObject;
to?: ApObject; to?: ApObject;
@ -34,6 +58,91 @@ export interface IObject {
href?: string; href?: string;
tag?: IObject | IObject[]; tag?: IObject | IObject[];
sensitive?: boolean; sensitive?: boolean;
visibility?: string;
mentionedUsers?: any[];
visibleUsers?: any[];
}
function sanitizeObj<T extends object> (obj: T): T {
const unsafe = ['__proto__', 'constructor', 'prototype'];
for (const key of unsafe) {
if (key in obj) {
throw new Error(`unsafe key: ${key}`);
}
}
return obj;
}
export interface IObject extends IUnsanitizedObject {
__yume_normalized_object: true | 'outgoing';
};
export interface YumeDowncastSanitizedBadge<L extends 'question' | undefined> {
__yume_normalized_badge: L | 'outgoing';
};
export function markOutgoing<T, L extends 'question' | undefined>(object: T, _badge: L): T & IObject & YumeDowncastSanitizedBadge<L> {
return object as T & IObject & YumeDowncastSanitizedBadge<L>;
}
export function yumeNormalizeURL(url: string): string {
const u = new URL(url);
u.hash = '';
u.host = toASCII(u.host);
if (u.protocol && u.protocol !== 'https:') {
throw new Error('protocol is not https');
}
u.protocol = 'https:';
if (u.port && u.port !== '443') {
throw new Error('port is not 443');
}
return u.toString();
}
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
IObject | string | (IObject | string)[] {
if (depth > 16) {
throw new Error('recursion limit exceeded');
}
if (typeof object === 'string') {
return yumeNormalizeURL(object);
}
if (Array.isArray(object)) {
if (object.length > 64) {
throw new Error('array length limit exceeded');
}
return object.flatMap(yumeNormalizeRecursive);
}
return yumeNormalizeObject(object);
}
export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
if (object.cc) {
object.cc = yumeNormalizeRecursive(object.cc);
}
if (object.id) {
object.id = yumeNormalizeURL(object.id);
}
if (object.url) {
object.url = yumeNormalizeRecursive(object.url);
}
if (object.attachment) {
object.attachment = object.attachment.map(yumeNormalizeRecursive);
}
if (object.inReplyTo) {
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
}
return {
...sanitizeObj(object),
__yume_normalized_object: true,
};
} }
/** /**
@ -80,7 +189,7 @@ export function getOneApHrefNullable(value: ApObject | undefined): string | unde
} }
export function getApHrefNullable(value: string | IObject | undefined): string | undefined { export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
if (typeof value?.href === 'string') return value.href; if (typeof value?.href === 'string') return value.href;
return undefined; return undefined;
} }
@ -101,6 +210,24 @@ export interface IActivity extends IObject {
}; };
} }
export interface SafeList {
id: string;
published: string;
visibility: string;
mentionedUsers: any[];
visibleUsers: any[];
}
function extractSafe(object: IObject): Partial<SafeList> {
return {
id: object.id,
published: object.published,
visibility: object.visibility,
mentionedUsers: object.mentionedUsers,
visibleUsers: object.visibleUsers,
};
}
export interface ICollection extends IObject { export interface ICollection extends IObject {
type: 'Collection'; type: 'Collection';
totalItems: number; totalItems: number;
@ -122,7 +249,7 @@ export const isPost = (object: IObject): object is IPost => {
return type != null && validPost.includes(type); return type != null && validPost.includes(type);
}; };
export interface IPost extends IObject { export interface IPost extends IObject{
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
source?: { source?: {
content: string; content: string;
@ -133,7 +260,7 @@ export interface IPost extends IObject {
quoteUrl?: string; quoteUrl?: string;
} }
export interface IQuestion extends IObject { export interface IUnsanitizedQuestion extends IObject {
type: 'Note' | 'Question'; type: 'Note' | 'Question';
actor: string; actor: string;
source?: { source?: {
@ -148,7 +275,25 @@ export interface IQuestion extends IObject {
closed?: Date; closed?: Date;
} }
export const isQuestion = (object: IObject): object is IQuestion => export interface IQuestion extends IUnsanitizedQuestion, YumeDowncastSanitizedBadge<'question'> {}
export function yumeSanitizeQuestion(object: IUnsanitizedQuestion): IQuestion {
return {
type: object.type,
actor: yumeNormalizeURL(object.actor),
source: object.source,
_misskey_quote: object._misskey_quote,
quoteUrl: object.quoteUrl ? yumeNormalizeURL(object.quoteUrl) : '',
oneOf: object.oneOf,
anyOf: object.anyOf,
endTime: object.endTime,
closed: object.closed,
__yume_normalized_object: true,
__yume_normalized_badge: 'question',
};
}
export const isQuestion = (object: IObject): object is IUnsanitizedQuestion =>
getApType(object) === 'Note' || getApType(object) === 'Question'; getApType(object) === 'Note' || getApType(object) === 'Question';
interface IQuestionChoice { interface IQuestionChoice {
@ -264,88 +409,307 @@ export const isDocument = (object: IObject): object is IApDocument => {
return type != null && validDocumentTypes.includes(type); return type != null && validDocumentTypes.includes(type);
}; };
export interface IApImage extends IApDocument { export interface IApImage extends IApDocument, Partial<SafeList> {
type: 'Image'; type: 'Image';
} }
export interface ICreate extends IActivity { export interface ICreate extends IActivity, Partial<SafeList> {
type: 'Create'; type: 'Create';
} }
export interface IDelete extends IActivity { export interface IDelete extends IActivity, Partial<SafeList> {
type: 'Delete'; type: 'Delete';
} }
export interface IUpdate extends IActivity { export interface IUpdate extends IActivity, Partial<SafeList> {
type: 'Update'; type: 'Update';
} }
export interface IRead extends IActivity { export interface IRead extends IActivity, Partial<SafeList> {
type: 'Read'; type: 'Read';
} }
export interface IUndo extends IActivity { export interface IUndo extends IActivity, Partial<SafeList> {
type: 'Undo'; type: 'Undo';
} }
export interface IFollow extends IActivity { export interface IFollow extends IActivity, Partial<SafeList> {
type: 'Follow'; type: 'Follow';
} }
export interface IAccept extends IActivity { export interface IAccept extends IActivity, Partial<SafeList> {
type: 'Accept'; type: 'Accept';
} }
export interface IReject extends IActivity { export interface IReject extends IActivity, Partial<SafeList> {
type: 'Reject'; type: 'Reject';
} }
export interface IAdd extends IActivity { export interface IAdd extends IActivity, Partial<SafeList> {
type: 'Add'; type: 'Add';
} }
export interface IRemove extends IActivity { export interface IRemove extends IActivity, Partial<SafeList> {
type: 'Remove'; type: 'Remove';
} }
export interface ILike extends IActivity { export interface ILike extends IActivity, Partial<SafeList> {
type: 'Like' | 'EmojiReaction' | 'EmojiReact'; type: 'Like' | 'EmojiReaction' | 'EmojiReact';
_misskey_reaction?: string; _misskey_reaction?: string;
} }
export interface IAnnounce extends IActivity { export interface IAnnounce extends IActivity, Partial<SafeList> {
type: 'Announce'; type: 'Announce';
} }
export interface IBlock extends IActivity { export interface IBlock extends IActivity, Partial<SafeList> {
type: 'Block'; type: 'Block';
} }
export interface IFlag extends IActivity { export interface IFlag extends IActivity, Partial<SafeList> {
type: 'Flag'; type: 'Flag';
} }
export interface IMove extends IActivity { export interface IMove extends IActivity, Partial<SafeList> {
type: 'Move'; type: 'Move';
target: IObject | string; target: IObject | string;
} }
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; export function yumeDowncastCreate(object: IObject): ICreate | null {
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; if (getApType(object) !== 'Create') return null;
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; const obj = object as ICreate;
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; if (!obj.actor || !obj.object) return null;
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; return {
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; ...extractMisskeyVendorKeys(object),
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; ...extractSafe(object),
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; type: 'Create',
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
export const isLike = (object: IObject): object is ILike => { target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
const type = getApType(object); __yume_normalized_object: true,
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); };
}; }
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export function yumeDowncastDelete(object: IObject): IDelete | null {
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; if (getApType(object) !== 'Delete') return null;
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; const obj = object as IDelete;
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note'; if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Delete',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
if (getApType(object) !== 'Update') return null;
const obj = object as IUpdate;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Update',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastRead(object: IObject): IRead | null {
if (getApType(object) !== 'Read') return null;
const obj = object as IRead;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Read',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastUndo(object: IObject): IUndo | null {
if (getApType(object) !== 'Undo') return null;
const obj = object as IUndo;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Undo',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastFollow(object: IObject): IFollow | null {
if (getApType(object) !== 'Follow') return null;
const obj = object as IFollow;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Follow',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAccept(object: IObject): IAccept | null {
if (getApType(object) !== 'Accept') return null;
const obj = object as IAccept;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Accept',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastReject(object: IObject): IReject | null {
if (getApType(object) !== 'Reject') return null;
const obj = object as IReject;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Reject',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAdd(object: IObject): IAdd | null {
if (getApType(object) !== 'Add') return null;
const obj = object as IAdd;
if (!obj.actor || !obj.object ) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Add',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastRemove(object: IObject): IRemove | null {
if (getApType(object) !== 'Remove') return null;
const obj = object as IRemove;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Remove',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastLike(object: IObject): ILike | null {
if (getApType(object) !== 'Like') return null;
const obj = object as ILike;
if (!obj.actor || !obj.object || !obj.target) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Like',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
if (getApType(object) !== 'Announce') return null;
const obj = object as IAnnounce;
if (!obj.actor || !obj.object) return null;
return {
// ...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Announce',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastBlock(object: IObject): IBlock | null {
if (getApType(object) !== 'Block') return null;
const obj = object as IBlock;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Block',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastFlag(object: IObject): IFlag | null {
if (getApType(object) !== 'Flag') return null;
const obj = object as IFlag;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Flag',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastMove(object: IObject): IMove | null {
if (getApType(object) !== 'Move') return null;
const obj = object as IMove;
if (!obj.actor || !obj.object || !obj.target) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Move',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target),
__yume_normalized_object: true,
};
}
export function yumeDowncastMention(object: IObject): IApMention | null {
if (getApType(object) !== 'Mention') {
return null;
}
const href = getApHrefNullable(object);
return {
...object,
type: 'Mention',
href: href ? yumeNormalizeURL(href) : '',
name: object.name ?? '',
};
}

View file

@ -67,7 +67,7 @@ const mIncomingApReject = metricCounter({
const mincomingApProcessingError = metricCounter({ const mincomingApProcessingError = metricCounter({
name: 'misskey_incoming_ap_processing_error', name: 'misskey_incoming_ap_processing_error',
help: 'Incoming AP processing error', help: 'Incoming AP processing error',
labelNames: ['incoming_host', 'incoming_type'], labelNames: ['incoming_host', 'incoming_type', 'reason'],
}); });
@Injectable() @Injectable()

View file

@ -134,8 +134,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.mergePack( return await this.mergePack(
me, me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, isPost(object) ? await this.apNoteService.createNote(getApId(object), resolver, true) : null,
); );
} }

View file

@ -19,7 +19,7 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; import { yumeNormalizeObject, type IActor, type IApDocument, type ICollection, type IObject, type IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
@ -42,6 +42,7 @@ function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
id: actorId, id: actorId,
type: 'Person', type: 'Person',
preferredUsername, preferredUsername,
__yume_normalized_object: true,
inbox: `${actorId}/inbox`, inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`, outbox: `${actorId}/outbox`,
}; };
@ -55,6 +56,7 @@ function createRandomNote(actor: NonTransientIActor): NonTransientIPost {
id: noteId, id: noteId,
type: 'Note', type: 'Note',
attributedTo: actor.id, attributedTo: actor.id,
__yume_normalized_object: true,
content: 'test test foo', content: 'test test foo',
}; };
} }
@ -71,6 +73,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
type: 'Collection', type: 'Collection',
id: actor.outbox as string, id: actor.outbox as string,
totalItems: items.length, totalItems: items.length,
__yume_normalized_object: true,
items, items,
}; };
} }
@ -162,6 +165,34 @@ describe('ActivityPub', () => {
content: 'あ', content: 'あ',
}; };
const punnyPost = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://あ.com/users/あ`,
type: 'Note',
attributedTo: actor.id,
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
};
test('punnyPost normalization', async () => {
const normalized = yumeNormalizeObject(punnyPost);
assert.strictEqual(normalized.id, 'https://xn--l8j.com/users/あ');
});
const portedHost = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://あ.com:12443/users/${secureRndstr(8)}`,
type: 'Note',
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
}
test('actor with port should be rejected', async () => {
assert.throws(() => {
yumeNormalizeObject(portedHost);
});
});
test('Minimum Actor', async () => { test('Minimum Actor', async () => {
resolver.register(actor.id, actor); resolver.register(actor.id, actor);
@ -220,6 +251,7 @@ describe('ActivityPub', () => {
type: 'OrderedCollection', type: 'OrderedCollection',
totalItems: 0, totalItems: 0,
first: `${actor.id}/following?page=1`, first: `${actor.id}/following?page=1`,
__yume_normalized_object: true,
}; };
actor.followers = `${actor.id}/followers`; actor.followers = `${actor.id}/followers`;
@ -229,6 +261,7 @@ describe('ActivityPub', () => {
type: 'OrderedCollection', type: 'OrderedCollection',
totalItems: 0, totalItems: 0,
first: `${actor.followers}?page=1`, first: `${actor.followers}?page=1`,
__yume_normalized_object: true,
}); });
const user = await personService.createPerson(actor.id, resolver); const user = await personService.createPerson(actor.id, resolver);
@ -244,6 +277,7 @@ describe('ActivityPub', () => {
id: `${actor.id}/following`, id: `${actor.id}/following`,
type: 'OrderedCollection', type: 'OrderedCollection',
totalItems: 0, totalItems: 0,
__yume_normalized_object: true,
// first: … // first: …
}; };
actor.followers = `${actor.id}/followers`; actor.followers = `${actor.id}/followers`;
@ -348,6 +382,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png', mediaType: 'image/png',
url: 'http://host1.test/foo.png', url: 'http://host1.test/foo.png',
name: '', name: '',
__yume_normalized_object: true,
}; };
const driveFile = await imageService.createImage( const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -361,6 +396,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png', url: 'http://host1.test/bar.png',
name: '', name: '',
sensitive: true, sensitive: true,
__yume_normalized_object: true,
}; };
const sensitiveDriveFile = await imageService.createImage( const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -377,6 +413,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png', mediaType: 'image/png',
url: 'http://host1.test/foo.png', url: 'http://host1.test/foo.png',
name: '', name: '',
__yume_normalized_object: true,
}; };
const driveFile = await imageService.createImage( const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -390,6 +427,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png', url: 'http://host1.test/bar.png',
name: '', name: '',
sensitive: true, sensitive: true,
__yume_normalized_object: true,
}; };
const sensitiveDriveFile = await imageService.createImage( const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -406,6 +444,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png', mediaType: 'image/png',
url: 'http://host1.test/foo.png', url: 'http://host1.test/foo.png',
name: '', name: '',
__yume_normalized_object: true,
}; };
const driveFile = await imageService.createImage( const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -419,6 +458,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png', url: 'http://host1.test/bar.png',
name: '', name: '',
sensitive: true, sensitive: true,
__yume_normalized_object: true,
}; };
const sensitiveDriveFile = await imageService.createImage( const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),
@ -431,6 +471,7 @@ describe('ActivityPub', () => {
const linkObject: IObject = { const linkObject: IObject = {
type: 'Link', type: 'Link',
href: 'https://example.com/', href: 'https://example.com/',
__yume_normalized_object: true,
}; };
const driveFile = await imageService.createImage( const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService), await createRandomRemoteUser(resolver, personService),