type-safe sanitization of AP objects
Some checks failed
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
Lint / pnpm_install (push) Successful in 1m32s
Publish Docker image / Build (push) Successful in 4m19s
Test (production install and build) / production (22.11.0) (push) Successful in 55s
Lint / lint (backend) (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Some checks failed
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
Lint / pnpm_install (push) Successful in 1m32s
Publish Docker image / Build (push) Successful in 4m19s
Test (production install and build) / production (22.11.0) (push) Successful in 55s
Lint / lint (backend) (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
7a7aef71cd
commit
727a7fc320
11 changed files with 581 additions and 154 deletions
|
@ -18,7 +18,7 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
|
|||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||
import type { IObject } from '@/core/activitypub/type.js';
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
export type HttpRequestSendOptions = {
|
||||
throwErrorWhenResponseNotOk: boolean;
|
||||
|
@ -183,6 +183,16 @@ export class HttpRequestService {
|
|||
controller.abort();
|
||||
}, 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, {
|
||||
method: args.method ?? 'GET',
|
||||
headers: {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
|
|||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApResolverService } from './activitypub/ApResolverService.js';
|
||||
|
||||
@Injectable()
|
||||
export class RemoteUserResolveService {
|
||||
|
@ -35,6 +36,7 @@ export class RemoteUserResolveService {
|
|||
private remoteLoggerService: RemoteLoggerService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private apPersonService: ApPersonService,
|
||||
private apResolverService: ApResolverService,
|
||||
) {
|
||||
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)}`);
|
||||
return await this.apPersonService.createPerson(self.href);
|
||||
return await this.apPersonService.createPerson(self.href, this.apResolverService.createResolver());
|
||||
}
|
||||
|
||||
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
||||
|
|
|
@ -29,7 +29,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import type { 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';
|
||||
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 { ApLoggerService } from './ApLoggerService.js';
|
||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
|
@ -138,53 +138,93 @@ export class ApInboxService {
|
|||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (isCreate(activity)) {
|
||||
const create = yumeDowncastCreate(activity);
|
||||
if (create) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'create' });
|
||||
return await this.create(actor, activity);
|
||||
} else if (isDelete(activity)) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
|
||||
return await this.delete(actor, activity);
|
||||
} else if (isUpdate(activity)) {
|
||||
return await this.create(actor, create);
|
||||
}
|
||||
|
||||
const update = yumeDowncastUpdate(activity);
|
||||
if (update) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'update' });
|
||||
return await this.update(actor, activity);
|
||||
} else if (isFollow(activity)) {
|
||||
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, activity);
|
||||
} else if (isAccept(activity)) {
|
||||
return await this.follow(actor, follow);
|
||||
}
|
||||
|
||||
const accept = yumeDowncastAccept(activity);
|
||||
if (accept) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
|
||||
return await this.accept(actor, activity);
|
||||
} else if (isReject(activity)) {
|
||||
return await this.accept(actor, accept);
|
||||
}
|
||||
|
||||
const reject = yumeDowncastReject(activity);
|
||||
if (reject) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
|
||||
return await this.reject(actor, activity);
|
||||
} else if (isAdd(activity)) {
|
||||
return await this.reject(actor, reject);
|
||||
}
|
||||
|
||||
const add = yumeDowncastAdd(activity);
|
||||
if (add) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'add' });
|
||||
return await this.add(actor, activity);
|
||||
} else if (isRemove(activity)) {
|
||||
return await this.add(actor, add);
|
||||
}
|
||||
|
||||
const remove = yumeDowncastRemove(activity);
|
||||
if (remove) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
|
||||
return await this.remove(actor, activity);
|
||||
} else if (isAnnounce(activity)) {
|
||||
return await this.remove(actor, remove);
|
||||
}
|
||||
|
||||
const announce = yumeDowncastAnnounce(activity);
|
||||
if (announce) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
|
||||
return await this.announce(actor, activity);
|
||||
} else if (isLike(activity)) {
|
||||
return await this.announce(actor, announce);
|
||||
}
|
||||
|
||||
const like = yumeDowncastLike(activity);
|
||||
if (like) {
|
||||
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)) {
|
||||
return await this.like(actor, like);
|
||||
}
|
||||
|
||||
const move = yumeDowncastMove(activity);
|
||||
if (move) {
|
||||
mInboxReceived?.inc({ host: actor.host, type: 'move' });
|
||||
return await this.move(actor, activity);
|
||||
} else {
|
||||
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
|
||||
private async follow(actor: MiRemoteUser, activity: IFollow): Promise<string> {
|
||||
|
@ -234,7 +274,8 @@ export class ApInboxService {
|
|||
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)}`;
|
||||
}
|
||||
|
@ -583,7 +624,8 @@ export class ApInboxService {
|
|||
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)}`;
|
||||
}
|
||||
|
@ -650,11 +692,20 @@ export class ApInboxService {
|
|||
});
|
||||
|
||||
// don't queue because the sender may attempt again when timeout
|
||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
||||
if (isLike(object)) return await this.undoLike(actor, object);
|
||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
||||
const follow = yumeDowncastFollow(object);
|
||||
if (follow) return await this.undoFollow(actor, follow);
|
||||
|
||||
const block = yumeDowncastBlock(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)}`;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.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()
|
||||
export class ApRendererService {
|
||||
|
@ -66,21 +66,21 @@ export class ApRendererService {
|
|||
|
||||
@bindThis
|
||||
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Accept',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Add',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
target,
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -103,7 +103,7 @@ export class ApRendererService {
|
|||
throw new Error('renderAnnounce: cannot render non-public note');
|
||||
}
|
||||
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Announce',
|
||||
|
@ -111,7 +111,7 @@ export class ApRendererService {
|
|||
to,
|
||||
cc,
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,23 +125,23 @@ export class ApRendererService {
|
|||
throw new Error('renderBlock: missing blockee uri');
|
||||
}
|
||||
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Block',
|
||||
id: `${this.config.url}/blocks/${block.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
||||
object: block.blockee.uri,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderCreate(object: IObject, note: MiNote): ICreate {
|
||||
const activity: ICreate = {
|
||||
const activity: ICreate = markOutgoing({
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Create',
|
||||
published: this.idService.parse(note.id).date.toISOString(),
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
|
||||
if (object.to) activity.to = object.to;
|
||||
if (object.cc) activity.cc = object.cc;
|
||||
|
@ -151,28 +151,28 @@ export class ApRendererService {
|
|||
|
||||
@bindThis
|
||||
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Delete',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderDocument(file: MiDriveFile): IApDocument {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Document',
|
||||
mediaType: file.webpublicType ?? file.type,
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
sensitive: file.isSensitive,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
||||
return {
|
||||
return markOutgoing( {
|
||||
id: `${this.config.url}/emojis/${emoji.name}`,
|
||||
type: 'Emoji',
|
||||
name: `:${emoji.name}:`,
|
||||
|
@ -183,28 +183,28 @@ export class ApRendererService {
|
|||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
},
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
// to anonymise reporters, the reporting actor must be a system user
|
||||
@bindThis
|
||||
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Flag',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
content,
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
||||
type: 'Follow',
|
||||
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,36 +223,36 @@ export class ApRendererService {
|
|||
followee: MiPartialLocalUser | MiPartialRemoteUser,
|
||||
requestId?: string,
|
||||
): IFollow {
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||
type: 'Follow',
|
||||
actor: this.userEntityService.getUserUri(follower),
|
||||
object: this.userEntityService.getUserUri(followee),
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderHashtag(tag: string): IApHashtag {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Hashtag',
|
||||
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
|
||||
name: `#${tag}`,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderImage(file: MiDriveFile): IApImage {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Image',
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
sensitive: file.isSensitive,
|
||||
name: file.comment,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
||||
type: 'Key',
|
||||
owner: this.userEntityService.genLocalUserUri(user.id),
|
||||
|
@ -260,21 +260,21 @@ export class ApRendererService {
|
|||
type: 'spki',
|
||||
format: 'pem',
|
||||
}),
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||
const reaction = noteReaction.reaction;
|
||||
|
||||
const object: ILike = {
|
||||
const object: ILike = markOutgoing({
|
||||
type: 'Like',
|
||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||
content: reaction,
|
||||
_misskey_reaction: reaction,
|
||||
};
|
||||
}, undefined);
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
|
@ -288,11 +288,11 @@ export class ApRendererService {
|
|||
|
||||
@bindThis
|
||||
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Mention',
|
||||
href: this.userEntityService.getUserUri(mention),
|
||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -302,13 +302,13 @@ export class ApRendererService {
|
|||
): IMove {
|
||||
const actor = this.userEntityService.getUserUri(src);
|
||||
const target = this.userEntityService.getUserUri(dst);
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||
actor,
|
||||
type: 'Move',
|
||||
object: actor,
|
||||
target,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -422,7 +422,7 @@ export class ApRendererService {
|
|||
})),
|
||||
} as const : {};
|
||||
|
||||
return {
|
||||
return markOutgoing({
|
||||
id: `${this.config.url}/notes/${note.id}`,
|
||||
type: 'Note',
|
||||
attributedTo,
|
||||
|
@ -445,7 +445,7 @@ export class ApRendererService {
|
|||
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
||||
tag,
|
||||
...asPoll,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -529,7 +529,7 @@ export class ApRendererService {
|
|||
|
||||
@bindThis
|
||||
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Question',
|
||||
id: `${this.config.url}/questions/${note.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
|
@ -542,78 +542,78 @@ export class ApRendererService {
|
|||
totalItems: poll.votes[i],
|
||||
},
|
||||
})),
|
||||
};
|
||||
}, 'question');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Reject',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Remove',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
target,
|
||||
object,
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderTombstone(id: string): ITombstone {
|
||||
return {
|
||||
return markOutgoing({
|
||||
id,
|
||||
type: 'Tombstone',
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
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;
|
||||
|
||||
return {
|
||||
return markOutgoing({
|
||||
type: 'Undo',
|
||||
...(id ? { id } : {}),
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
|
||||
return {
|
||||
return markOutgoing( {
|
||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
type: 'Update',
|
||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
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`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
type: 'Create',
|
||||
to: [pollOwner.uri],
|
||||
published: new Date().toISOString(),
|
||||
object: {
|
||||
object: markOutgoing({
|
||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
||||
type: 'Note',
|
||||
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
||||
to: [pollOwner.uri],
|
||||
inReplyTo: note.uri,
|
||||
name: poll.choices[vote.choice],
|
||||
},
|
||||
};
|
||||
}, undefined),
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -16,11 +16,11 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { isCollectionOrOrderedCollection } from './type.js';
|
||||
import { isCollectionOrOrderedCollection, yumeNormalizeObject } from './type.js';
|
||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
import { ApRendererService } from './ApRendererService.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 {
|
||||
private history: Set<string>;
|
||||
|
@ -67,7 +67,7 @@ export class Resolver {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
public async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
@ -103,8 +103,8 @@ export class Resolver {
|
|||
}
|
||||
|
||||
const object = (this.user
|
||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
||||
? await this.apRequestService.signedGet(value, this.user) as IUnsanitizedObject
|
||||
: await this.httpRequestService.getActivityJson(value)) as IUnsanitizedObject;
|
||||
|
||||
if (
|
||||
Array.isArray(object['@context']) ?
|
||||
|
@ -117,6 +117,13 @@ export class Resolver {
|
|||
return object;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
const object = await this.resolveNotNormalized(value);
|
||||
|
||||
return yumeNormalizeObject(object);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private resolveLocal(url: string): Promise<IObject> {
|
||||
const parsed = this.apDbResolverService.parseUri(url);
|
||||
|
|
|
@ -115,10 +115,7 @@ export class ApNoteService {
|
|||
* Noteを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
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();
|
||||
|
||||
public async createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<MiNote | null> {
|
||||
const object = await resolver.resolve(value);
|
||||
|
||||
const entryUri = getApId(value);
|
||||
|
@ -356,7 +353,7 @@ export class ApNoteService {
|
|||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常に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 {
|
||||
unlock();
|
||||
}
|
||||
|
|
|
@ -277,16 +277,13 @@ export class ApPersonService implements OnModuleInit {
|
|||
* Personを作成します。
|
||||
*/
|
||||
@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 (uri.startsWith(this.config.url)) {
|
||||
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);
|
||||
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
||||
|
||||
|
|
|
@ -3,20 +3,44 @@
|
|||
* 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 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[];
|
||||
type: string | string[];
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
summary?: string;
|
||||
_misskey_summary?: string;
|
||||
_misskey_followedMessage?: string | null;
|
||||
_misskey_requireSigninToViewContents?: boolean;
|
||||
_misskey_makeNotesFollowersOnlyBefore?: number | null;
|
||||
_misskey_makeNotesHiddenBefore?: number | null;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
|
@ -36,6 +60,83 @@ export interface IObject {
|
|||
sensitive?: boolean;
|
||||
}
|
||||
|
||||
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.to) {
|
||||
object.to = yumeNormalizeRecursive(object.to);
|
||||
}
|
||||
if (object.attributedTo) {
|
||||
object.attributedTo = yumeNormalizeRecursive(object.attributedTo);
|
||||
}
|
||||
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 {
|
||||
...object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of ActivityStreams Objects id
|
||||
*/
|
||||
|
@ -80,7 +181,7 @@ export function getOneApHrefNullable(value: ApObject | undefined): string | unde
|
|||
}
|
||||
|
||||
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;
|
||||
return undefined;
|
||||
}
|
||||
|
@ -122,7 +223,7 @@ export const isPost = (object: IObject): object is IPost => {
|
|||
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';
|
||||
source?: {
|
||||
content: string;
|
||||
|
@ -133,7 +234,7 @@ export interface IPost extends IObject {
|
|||
quoteUrl?: string;
|
||||
}
|
||||
|
||||
export interface IQuestion extends IObject {
|
||||
export interface IUnsanitizedQuestion extends IObject {
|
||||
type: 'Note' | 'Question';
|
||||
actor: string;
|
||||
source?: {
|
||||
|
@ -148,7 +249,25 @@ export interface IQuestion extends IObject {
|
|||
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';
|
||||
|
||||
interface IQuestionChoice {
|
||||
|
@ -330,22 +449,225 @@ export interface IMove extends IActivity {
|
|||
target: IObject | string;
|
||||
}
|
||||
|
||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
|
||||
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
|
||||
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
|
||||
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
|
||||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
||||
export const isLike = (object: IObject): object is ILike => {
|
||||
const type = getApType(object);
|
||||
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 const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
|
||||
export function yumeDowncastCreate(object: IObject): ICreate | null {
|
||||
if (getApType(object) !== 'Create') return null;
|
||||
const obj = object as ICreate;
|
||||
if (!obj.actor || !obj.object) return null;
|
||||
return {
|
||||
...extractMisskeyVendorKeys(object),
|
||||
type: 'Create',
|
||||
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 yumeDowncastDelete(object: IObject): IDelete | null {
|
||||
if (getApType(object) !== 'Delete') return null;
|
||||
const obj = object as IDelete;
|
||||
if (!obj.actor || !obj.object) return null;
|
||||
return {
|
||||
...extractMisskeyVendorKeys(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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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 {
|
||||
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 ?? '',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ const mIncomingApReject = metricCounter({
|
|||
const mincomingApProcessingError = metricCounter({
|
||||
name: 'misskey_incoming_ap_processing_error',
|
||||
help: 'Incoming AP processing error',
|
||||
labelNames: ['incoming_host', 'incoming_type'],
|
||||
labelNames: ['incoming_host', 'incoming_type', 'reason'],
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
|
|
|
@ -134,8 +134,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
return await this.mergePack(
|
||||
me,
|
||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), resolver, true) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
|||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.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 { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
@ -42,6 +42,7 @@ function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
|
|||
id: actorId,
|
||||
type: 'Person',
|
||||
preferredUsername,
|
||||
__yume_normalized_object: true,
|
||||
inbox: `${actorId}/inbox`,
|
||||
outbox: `${actorId}/outbox`,
|
||||
};
|
||||
|
@ -55,6 +56,7 @@ function createRandomNote(actor: NonTransientIActor): NonTransientIPost {
|
|||
id: noteId,
|
||||
type: 'Note',
|
||||
attributedTo: actor.id,
|
||||
__yume_normalized_object: true,
|
||||
content: 'test test foo',
|
||||
};
|
||||
}
|
||||
|
@ -71,6 +73,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
|
|||
type: 'Collection',
|
||||
id: actor.outbox as string,
|
||||
totalItems: items.length,
|
||||
__yume_normalized_object: true,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
@ -162,6 +165,34 @@ describe('ActivityPub', () => {
|
|||
content: 'あ',
|
||||
};
|
||||
|
||||
const punnyPost = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `https://あ.com/users/${secureRndstr(8)}`,
|
||||
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 () => {
|
||||
resolver.register(actor.id, actor);
|
||||
|
||||
|
@ -220,6 +251,7 @@ describe('ActivityPub', () => {
|
|||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.id}/following?page=1`,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
||||
|
@ -229,6 +261,7 @@ describe('ActivityPub', () => {
|
|||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.followers}?page=1`,
|
||||
__yume_normalized_object: true,
|
||||
});
|
||||
|
||||
const user = await personService.createPerson(actor.id, resolver);
|
||||
|
@ -244,6 +277,7 @@ describe('ActivityPub', () => {
|
|||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
__yume_normalized_object: true,
|
||||
// first: …
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
@ -348,6 +382,7 @@ describe('ActivityPub', () => {
|
|||
mediaType: 'image/png',
|
||||
url: 'http://host1.test/foo.png',
|
||||
name: '',
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const driveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -361,6 +396,7 @@ describe('ActivityPub', () => {
|
|||
url: 'http://host1.test/bar.png',
|
||||
name: '',
|
||||
sensitive: true,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const sensitiveDriveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -377,6 +413,7 @@ describe('ActivityPub', () => {
|
|||
mediaType: 'image/png',
|
||||
url: 'http://host1.test/foo.png',
|
||||
name: '',
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const driveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -390,6 +427,7 @@ describe('ActivityPub', () => {
|
|||
url: 'http://host1.test/bar.png',
|
||||
name: '',
|
||||
sensitive: true,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const sensitiveDriveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -406,6 +444,7 @@ describe('ActivityPub', () => {
|
|||
mediaType: 'image/png',
|
||||
url: 'http://host1.test/foo.png',
|
||||
name: '',
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const driveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -419,6 +458,7 @@ describe('ActivityPub', () => {
|
|||
url: 'http://host1.test/bar.png',
|
||||
name: '',
|
||||
sensitive: true,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const sensitiveDriveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
@ -431,6 +471,7 @@ describe('ActivityPub', () => {
|
|||
const linkObject: IObject = {
|
||||
type: 'Link',
|
||||
href: 'https://example.com/',
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
const driveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
|
|
Loading…
Reference in a new issue