WIP: type-safe sanitization of AP objects
Some checks failed
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 1m41s
Publish Docker image / Build (push) Successful in 3m53s
Test (production install and build) / production (22.11.0) (push) Successful in 55s
Lint / lint (backend) (push) Successful in 1m56s
Test (backend) / unit (22.11.0) (push) Failing after 6m55s
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (push) Has been cancelled
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Some checks failed
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 1m41s
Publish Docker image / Build (push) Successful in 3m53s
Test (production install and build) / production (22.11.0) (push) Successful in 55s
Lint / lint (backend) (push) Successful in 1m56s
Test (backend) / unit (22.11.0) (push) Failing after 6m55s
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (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
8e9d75c1e4
9 changed files with 482 additions and 119 deletions
|
@ -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, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, 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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -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,10 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { toASCII } from "node:punycode";
|
||||
|
||||
export type Obj = { [x: string]: any };
|
||||
export type ApObject = IObject | string | (IObject | string)[];
|
||||
|
||||
export interface IObject {
|
||||
export interface IUnsanitizedObject {
|
||||
'@context'?: string | string[] | Obj | Obj[];
|
||||
type: string | string[];
|
||||
id?: string;
|
||||
|
@ -36,6 +38,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
|
||||
*/
|
||||
|
@ -133,7 +212,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 +227,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 {
|
||||
|
@ -349,3 +446,185 @@ export const isBlock = (object: IObject): object is IBlock => getApType(object)
|
|||
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;
|
||||
return {
|
||||
type: 'Create',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastDelete(object: IObject): IDelete | null {
|
||||
if (getApType(object) !== 'Delete') return null;
|
||||
const obj = object as IDelete;
|
||||
return {
|
||||
type: 'Delete',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
|
||||
if (getApType(object) !== 'Update') return null;
|
||||
const obj = object as IUpdate;
|
||||
return {
|
||||
type: 'Update',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastRead(object: IObject): IRead | null {
|
||||
if (getApType(object) !== 'Read') return null;
|
||||
const obj = object as IRead;
|
||||
return {
|
||||
type: 'Read',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastUndo(object: IObject): IUndo | null {
|
||||
if (getApType(object) !== 'Undo') return null;
|
||||
const obj = object as IUndo;
|
||||
return {
|
||||
type: 'Undo',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastFollow(object: IObject): IFollow | null {
|
||||
if (getApType(object) !== 'Follow') return null;
|
||||
const obj = object as IFollow;
|
||||
return {
|
||||
type: 'Follow',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastAccept(object: IObject): IAccept | null {
|
||||
if (getApType(object) !== 'Accept') return null;
|
||||
const obj = object as IAccept;
|
||||
return {
|
||||
type: 'Accept',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastReject(object: IObject): IReject | null {
|
||||
if (getApType(object) !== 'Reject') return null;
|
||||
const obj = object as IReject;
|
||||
return {
|
||||
type: 'Reject',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastAdd(object: IObject): IAdd | null {
|
||||
if (getApType(object) !== 'Add') return null;
|
||||
const obj = object as IAdd;
|
||||
return {
|
||||
type: 'Add',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
target: obj.target,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastRemove(object: IObject): IRemove | null {
|
||||
if (getApType(object) !== 'Remove') return null;
|
||||
const obj = object as IRemove;
|
||||
return {
|
||||
type: 'Remove',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
target: obj.target,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastLike(object: IObject): ILike | null {
|
||||
if (getApType(object) !== 'Like') return null;
|
||||
const obj = object as ILike;
|
||||
return {
|
||||
type: 'Like',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
|
||||
if (getApType(object) !== 'Announce') return null;
|
||||
const obj = object as IAnnounce;
|
||||
return {
|
||||
type: 'Announce',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastBlock(object: IObject): IBlock | null {
|
||||
if (getApType(object) !== 'Block') return null;
|
||||
const obj = object as IBlock;
|
||||
return {
|
||||
type: 'Block',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastFlag(object: IObject): IFlag | null {
|
||||
if (getApType(object) !== 'Flag') return null;
|
||||
const obj = object as IFlag;
|
||||
return {
|
||||
type: 'Flag',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function yumeDowncastMove(object: IObject): IMove | null {
|
||||
if (getApType(object) !== 'Move') return null;
|
||||
const obj = object as IMove;
|
||||
return {
|
||||
type: 'Move',
|
||||
actor: obj.actor,
|
||||
object: obj.object,
|
||||
target: obj.target,
|
||||
__yume_normalized_object: true,
|
||||
};
|
||||
}
|
||||
export function yumeDowncastMention(object: IObject): IApMention {
|
||||
if (getApType(object) !== 'Mention') {
|
||||
throw new Error('not a mention');
|
||||
}
|
||||
|
||||
const href = getApHrefNullable(object);
|
||||
|
||||
return {
|
||||
...object,
|
||||
type: 'Mention',
|
||||
href: href ? yumeNormalizeURL(href) : '',
|
||||
name: object.name ?? '',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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