2024.11.0-yumechinokuni.7 #41

Merged
yume merged 45 commits from develop into master 2024-11-22 10:16:03 -06:00
14 changed files with 682 additions and 177 deletions
Showing only changes of commit a2517d3d03 - Show all commits

View file

@ -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: {

View file

@ -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からやりなおして返す

View file

@ -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)}`;
}

View file

@ -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

View file

@ -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);

View file

@ -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();
}

View file

@ -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);

View file

@ -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;
@ -34,6 +58,78 @@ export interface IObject {
href?: string;
tag?: IObject | IObject[];
sensitive?: boolean;
visibility?: string;
mentionedUsers?: any[];
visibleUsers?: any[];
}
export interface IObject extends IUnsanitizedObject {
__yume_normalized_object: true | 'outgoing';
};
export interface YumeDowncastSanitizedBadge<L extends 'question' | undefined> {
__yume_normalized_badge: L | 'outgoing';
};
export function markOutgoing<T, L extends 'question' | undefined>(object: T, _badge: L): T & IObject & YumeDowncastSanitizedBadge<L> {
return object as T & IObject & YumeDowncastSanitizedBadge<L>;
}
export function yumeNormalizeURL(url: string): string {
const u = new URL(url);
u.hash = '';
u.host = toASCII(u.host);
if (u.protocol && u.protocol !== 'https:') {
throw new Error('protocol is not https');
}
u.protocol = 'https:';
if (u.port && u.port !== '443') {
throw new Error('port is not 443');
}
return u.toString();
}
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
IObject | string | (IObject | string)[] {
if (depth > 16) {
throw new Error('recursion limit exceeded');
}
if (typeof object === 'string') {
return yumeNormalizeURL(object);
}
if (Array.isArray(object)) {
if (object.length > 64) {
throw new Error('array length limit exceeded');
}
return object.flatMap(yumeNormalizeRecursive);
}
return yumeNormalizeObject(object);
}
export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
if (object.cc) {
object.cc = yumeNormalizeRecursive(object.cc);
}
if (object.id) {
object.id = yumeNormalizeURL(object.id);
}
if (object.url) {
object.url = yumeNormalizeRecursive(object.url);
}
if (object.attachment) {
object.attachment = object.attachment.map(yumeNormalizeRecursive);
}
if (object.inReplyTo) {
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
}
return object as IObject;
}
/**
@ -80,7 +176,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;
}
@ -101,6 +197,24 @@ export interface IActivity extends IObject {
};
}
export interface SafeList {
id: string;
published: string;
visibility: string;
mentionedUsers: any[];
visibleUsers: any[];
}
function extractSafe(object: IObject): Partial<SafeList> {
return {
id: object.id,
published: object.published,
visibility: object.visibility,
mentionedUsers: object.mentionedUsers,
visibleUsers: object.visibleUsers,
};
}
export interface ICollection extends IObject {
type: 'Collection';
totalItems: number;
@ -122,7 +236,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 +247,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 +262,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 {
@ -264,88 +396,307 @@ export const isDocument = (object: IObject): object is IApDocument => {
return type != null && validDocumentTypes.includes(type);
};
export interface IApImage extends IApDocument {
export interface IApImage extends IApDocument, Partial<SafeList> {
type: 'Image';
}
export interface ICreate extends IActivity {
export interface ICreate extends IActivity, Partial<SafeList> {
type: 'Create';
}
export interface IDelete extends IActivity {
export interface IDelete extends IActivity, Partial<SafeList> {
type: 'Delete';
}
export interface IUpdate extends IActivity {
export interface IUpdate extends IActivity, Partial<SafeList> {
type: 'Update';
}
export interface IRead extends IActivity {
export interface IRead extends IActivity, Partial<SafeList> {
type: 'Read';
}
export interface IUndo extends IActivity {
export interface IUndo extends IActivity, Partial<SafeList> {
type: 'Undo';
}
export interface IFollow extends IActivity {
export interface IFollow extends IActivity, Partial<SafeList> {
type: 'Follow';
}
export interface IAccept extends IActivity {
export interface IAccept extends IActivity, Partial<SafeList> {
type: 'Accept';
}
export interface IReject extends IActivity {
export interface IReject extends IActivity, Partial<SafeList> {
type: 'Reject';
}
export interface IAdd extends IActivity {
export interface IAdd extends IActivity, Partial<SafeList> {
type: 'Add';
}
export interface IRemove extends IActivity {
export interface IRemove extends IActivity, Partial<SafeList> {
type: 'Remove';
}
export interface ILike extends IActivity {
export interface ILike extends IActivity, Partial<SafeList> {
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
_misskey_reaction?: string;
}
export interface IAnnounce extends IActivity {
export interface IAnnounce extends IActivity, Partial<SafeList> {
type: 'Announce';
}
export interface IBlock extends IActivity {
export interface IBlock extends IActivity, Partial<SafeList> {
type: 'Block';
}
export interface IFlag extends IActivity {
export interface IFlag extends IActivity, Partial<SafeList> {
type: 'Flag';
}
export interface IMove extends IActivity {
export interface IMove extends IActivity, Partial<SafeList> {
type: 'Move';
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),
...extractSafe(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),
...extractSafe(object),
type: 'Delete',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
if (getApType(object) !== 'Update') return null;
const obj = object as IUpdate;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Update',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastRead(object: IObject): IRead | null {
if (getApType(object) !== 'Read') return null;
const obj = object as IRead;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Read',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastUndo(object: IObject): IUndo | null {
if (getApType(object) !== 'Undo') return null;
const obj = object as IUndo;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Undo',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastFollow(object: IObject): IFollow | null {
if (getApType(object) !== 'Follow') return null;
const obj = object as IFollow;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Follow',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAccept(object: IObject): IAccept | null {
if (getApType(object) !== 'Accept') return null;
const obj = object as IAccept;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Accept',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastReject(object: IObject): IReject | null {
if (getApType(object) !== 'Reject') return null;
const obj = object as IReject;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Reject',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAdd(object: IObject): IAdd | null {
if (getApType(object) !== 'Add') return null;
const obj = object as IAdd;
if (!obj.actor || !obj.object ) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Add',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastRemove(object: IObject): IRemove | null {
if (getApType(object) !== 'Remove') return null;
const obj = object as IRemove;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Remove',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastLike(object: IObject): ILike | null {
if (getApType(object) !== 'Like') return null;
const obj = object as ILike;
if (!obj.actor || !obj.object || !obj.target) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Like',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
if (getApType(object) !== 'Announce') return null;
const obj = object as IAnnounce;
if (!obj.actor || !obj.object) return null;
return {
// ...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Announce',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastBlock(object: IObject): IBlock | null {
if (getApType(object) !== 'Block') return null;
const obj = object as IBlock;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Block',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastFlag(object: IObject): IFlag | null {
if (getApType(object) !== 'Flag') return null;
const obj = object as IFlag;
if (!obj.actor || !obj.object) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Flag',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
__yume_normalized_object: true,
};
}
export function yumeDowncastMove(object: IObject): IMove | null {
if (getApType(object) !== 'Move') return null;
const obj = object as IMove;
if (!obj.actor || !obj.object || !obj.target) return null;
return {
...extractMisskeyVendorKeys(object),
...extractSafe(object),
type: 'Move',
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
target: typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target),
__yume_normalized_object: true,
};
}
export function yumeDowncastMention(object: IObject): IApMention | null {
if (getApType(object) !== 'Mention') {
return null;
}
const href = getApHrefNullable(object);
return {
...object,
type: 'Mention',
href: href ? yumeNormalizeURL(href) : '',
name: object.name ?? '',
};
}

View file

@ -11,15 +11,39 @@ import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js';
import { In } from 'typeorm';
import type { Config } from '@/config.js';
@Injectable()
export class EmojiEntityService {
constructor(
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.config)
private config: Config,
) {
}
private stripProxyIfOrigin(url: string): string {
try {
const u = new URL(url);
let origin = u.origin;
if (u.origin === new URL(this.config.mediaProxy).origin) {
const innerUrl = u.searchParams.get('url');
if (innerUrl) {
origin = new URL(innerUrl).origin;
}
}
if (origin === u.origin) {
return url;
}
} catch (e) {
return url;
}
return url;
}
@bindThis
public packSimpleNoQuery(
emoji: MiEmoji,
@ -29,7 +53,7 @@ export class EmojiEntityService {
name: emoji.name,
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
@ -72,7 +96,7 @@ export class EmojiEntityService {
category: emoji.category,
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
license: emoji.license,
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, ViewEntity } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@ -98,3 +98,4 @@ export class MiFollowing {
public followeeSharedInbox: string | null;
//#endregion
}

View file

@ -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()

View file

@ -33,7 +33,6 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import { makeHstsHook } from './hsts.js';
import { generateCSP } from './csp.js';
import * as prom from 'prom-client';
import { sanitizeRequestURI } from '@/misc/log-sanitization.js';
import { metricCounter, metricGauge, metricHistogram, MetricsService } from './api/MetricsService.js';
@ -110,6 +109,11 @@ const mLastSuccessfulRequest = metricGauge({
labelNames: [],
});
// This function is used to determine if a path is safe to redirect to.
function redirectSafePath(path: string): boolean {
return ['/files/', '/identicon/', '/proxy/', '/static-assets/', '/vite/', '/embed_vite/'].some(prefix => path.startsWith(prefix));
}
@Injectable()
export class ServerService implements OnApplicationShutdown {
private logger: Logger;
@ -348,7 +352,7 @@ export class ServerService implements OnApplicationShutdown {
name: name,
});
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
reply.header('Content-Security-Policy', 'default-src \'none\'');
if (emoji == null) {
if ('fallback' in request.query) {
@ -359,16 +363,26 @@ export class ServerService implements OnApplicationShutdown {
}
}
const dbUrl = emoji?.publicUrl || emoji?.originalUrl;
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
let url: URL;
if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('url', dbUrl);
url.searchParams.set('badge', '1');
} else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('url', dbUrl);
url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1');
}
@ -392,6 +406,16 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400');
if (user) {
const dbUrl = user?.avatarUrl ?? this.userEntityService.getIdenticonUrl(user);
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
} else {
reply.redirect('/static-assets/user-unknown.png');

View file

@ -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,
);
}

View file

@ -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/あ`,
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),