Merge remote-tracking branch 'misskey/release/2024.5.0' into future-2024-04-25-post

This commit is contained in:
dakkar 2024-05-11 14:13:07 +01:00
commit 451b0ecc9b
81 changed files with 16594 additions and 13171 deletions

View file

@ -127,7 +127,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js';
import { ApRendererService } from './activitypub/ApRendererService.js';
import { ApRequestService } from './activitypub/ApRequestService.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
import { LdSignatureService } from './activitypub/LdSignatureService.js';
import { JsonLdService } from './activitypub/JsonLdService.js';
import { RemoteLoggerService } from './RemoteLoggerService.js';
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
import { WebfingerService } from './WebfingerService.js';
@ -266,7 +266,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
@ -406,7 +406,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
LdSignatureService,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@ -542,7 +542,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
$LdSignatureService,
$JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,
@ -678,7 +678,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
LdSignatureService,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@ -813,7 +813,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
$LdSignatureService,
$JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,

View file

@ -22,7 +22,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Config } from '@/config.js';
import { DriveService } from './DriveService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {

View file

@ -6,7 +6,7 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5';
import { Window } from 'happy-dom';
import { Window, XMLSerializer } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
@ -247,6 +247,8 @@ export class MfmService {
const doc = window.document;
const body = doc.createElement('p');
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
@ -457,9 +459,9 @@ export class MfmService {
},
};
appendChildren(nodes, doc.body);
appendChildren(nodes, body);
return `<p>${doc.body.innerHTML}</p>`;
return new XMLSerializer().serializeToString(body);
}
// the toMastoHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version

View file

@ -10,7 +10,7 @@ import {
generateRegistrationOptions, verifyAuthenticationResponse,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
@ -49,7 +49,7 @@ export class WebAuthnService {
const instance = await this.metaService.fetch();
return {
origin: this.config.url,
rpId: this.config.host,
rpId: this.config.hostname,
rpName: instance.name ?? this.config.host,
rpIcon: instance.iconUrl ?? undefined,
};
@ -65,13 +65,12 @@ export class WebAuthnService {
const registrationOptions = await generateRegistrationOptions({
rpName: relyingParty.rpName,
rpID: relyingParty.rpId,
userID: userId,
userID: isoUint8Array.fromUTF8String(userId),
userName: userName,
userDisplayName: userDisplayName,
attestationType: 'indirect',
excludeCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
id: Buffer.from(key.id, 'base64url'),
type: 'public-key',
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
id: key.id,
transports: key.transports ?? undefined,
})),
authenticatorSelection: {
@ -87,7 +86,7 @@ export class WebAuthnService {
@bindThis
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
credentialID: Uint8Array;
credentialID: string;
credentialPublicKey: Uint8Array;
attestationObject: Uint8Array;
fmt: AttestationFormat;
@ -144,6 +143,7 @@ export class WebAuthnService {
@bindThis
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
const relyingParty = await this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
@ -153,9 +153,9 @@ export class WebAuthnService {
}
const authenticationOptions = await generateAuthenticationOptions({
allowCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
id: Buffer.from(key.id, 'base64url'),
type: 'public-key',
rpID: relyingParty.rpId,
allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
id: key.id,
transports: key.transports ?? undefined,
})),
userVerification: 'preferred',
@ -219,7 +219,7 @@ export class WebAuthnService {
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: Buffer.from(key.id, 'base64url'),
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,

View file

@ -29,7 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { MetaService } from '../MetaService.js';
import { LdSignatureService } from './LdSignatureService.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';
@ -61,7 +61,7 @@ export class ApRendererService {
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService,
private jsonLdService: JsonLdService,
private userKeypairService: UserKeypairService,
private apMfmService: ApMfmService,
private mfmService: MfmService,
@ -173,6 +173,7 @@ export class ApRendererService {
url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment,
summary: file.comment,
sensitive: file.isSensitive,
};
}
@ -794,9 +795,9 @@ export class ApRendererService {
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
const jsonLd = this.jsonLdService.use();
jsonLd.debug = false;
activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
return activity;
}

View file

@ -7,14 +7,14 @@ import * as crypto from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { CONTEXT, CONTEXTS } from './misc/contexts.js';
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
class LdSignature {
class JsonLd {
public debug = false;
public preLoad = true;
public loderTimeout = 5000;
@ -101,8 +101,6 @@ class LdSignature {
@bindThis
public async normalize(data: JsonLdDocument): Promise<string> {
const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
return (await import('jsonld')).default.normalize(data, {
documentLoader: customLoader,
});
@ -114,11 +112,11 @@ class LdSignature {
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
if (url in CONTEXTS) {
if (url in PRELOADED_CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`);
return {
contextUrl: undefined,
document: CONTEXTS[url],
document: PRELOADED_CONTEXTS[url],
documentUrl: url,
};
}
@ -135,7 +133,7 @@ class LdSignature {
}
@bindThis
private async fetchDocument(url: string): Promise<JsonLd> {
private async fetchDocument(url: string): Promise<JsonLdObject> {
const json = await this.httpRequestService.send(
url,
{
@ -156,7 +154,7 @@ class LdSignature {
}
});
return json as JsonLd;
return json as JsonLdObject;
}
@bindThis
@ -168,14 +166,14 @@ class LdSignature {
}
@Injectable()
export class LdSignatureService {
export class JsonLdService {
constructor(
private httpRequestService: HttpRequestService,
) {
}
@bindThis
public use(): LdSignature {
return new LdSignature(this.httpRequestService);
public use(): JsonLd {
return new JsonLd(this.httpRequestService);
}
}

View file

@ -570,7 +570,7 @@ const extension_context_definition = {
export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
export const CONTEXTS: Record<string, JsonLd> = {
export const PRELOADED_CONTEXTS: Record<string, JsonLd> = {
'https://w3id.org/identity/v1': id_v1,
'https://w3id.org/security/v1': security_v1,
'https://www.w3.org/ns/activitystreams': activitystreams,

View file

@ -18,7 +18,7 @@ import { checkHttps } from '@/misc/check-https.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
import type { IObject } from '../type.js';
import { isDocument, type IObject } from '../type.js';
@Injectable()
export class ApImageService {
@ -41,7 +41,7 @@ export class ApImageService {
* Imageを作成します
*/
@bindThis
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
@ -49,16 +49,18 @@ export class ApImageService {
const image = await this.apResolverService.createResolver().resolve(value);
if (!isDocument(image)) return null;
if (image.url == null) {
throw new Error('invalid image: url not provided');
return null;
}
if (typeof image.url !== 'string') {
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
return null;
}
if (!checkHttps(image.url)) {
throw new Error('invalid image: unexpected schema of url: ' + image.url);
return null;
}
this.logger.info(`Creating the Image: ${image.url}`);
@ -94,12 +96,11 @@ export class ApImageService {
/**
* Imageを解決します
*
* Misskeyに対象のImageが登録されていればそれを返し
* Misskeyに登録しそれを返します
* ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します
*/
@bindThis
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
// TODO
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
// TODO: Misskeyに対象のImageが登録されていればそれを返す
// リモートサーバーからフェッチしてきて登録
return await this.createImage(actor, value);

View file

@ -4,7 +4,6 @@
*/
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js';
@ -214,15 +213,13 @@ export class ApNoteService {
}
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
const limit = promiseLimit<MiDriveFile>(2);
const files = (await Promise.all(toArray(note.attachment).map(attach => (
limit(() => this.apImageService.resolveImage(actor, {
...attach,
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
}))
))));
const files: MiDriveFile[] = [];
for (const attach of toArray(note.attachment)) {
attach.sensitive ??= note.sensitive;
const file = await this.apImageService.resolveImage(actor, attach);
if (file) files.push(file);
}
// リプライ
const reply: MiNote | null = note.inReplyTo

View file

@ -25,6 +25,7 @@ export interface IObject {
endTime?: Date;
icon?: any;
image?: any;
mediaType?: string;
url?: ApObject | string;
href?: string;
tag?: IObject | IObject[];
@ -246,14 +247,14 @@ export interface IKey extends IObject {
}
export interface IApDocument extends IObject {
type: 'Document';
name: string | null;
mediaType: string;
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
}
export interface IApImage extends IObject {
export const isDocument = (object: IObject): object is IApDocument =>
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
export interface IApImage extends IApDocument {
type: 'Image';
name: string | null;
}
export interface ICreate extends IActivity {

View file

@ -257,20 +257,41 @@ export class UserEntityService implements OnModuleInit {
] = await Promise.all([
this.followingsRepository.findBy({ followerId: me })
.then(f => new Map(f.map(it => [it.followeeId, it]))),
this.followingsRepository.findBy({ followeeId: me })
.then(it => it.map(it => it.followerId)),
this.followRequestsRepository.findBy({ followerId: me })
.then(it => it.map(it => it.followeeId)),
this.followRequestsRepository.findBy({ followeeId: me })
.then(it => it.map(it => it.followerId)),
this.blockingsRepository.findBy({ blockerId: me })
.then(it => it.map(it => it.blockeeId)),
this.blockingsRepository.findBy({ blockeeId: me })
.then(it => it.map(it => it.blockerId)),
this.mutingsRepository.findBy({ muterId: me })
.then(it => it.map(it => it.muteeId)),
this.renoteMutingsRepository.findBy({ muterId: me })
.then(it => it.map(it => it.muteeId)),
this.followingsRepository.createQueryBuilder('f')
.select('f.followerId')
.where('f.followeeId = :me', { me })
.getRawMany<{ f_followerId: string }>()
.then(it => it.map(it => it.f_followerId)),
this.followRequestsRepository.createQueryBuilder('f')
.select('f.followeeId')
.where('f.followerId = :me', { me })
.getRawMany<{ f_followeeId: string }>()
.then(it => it.map(it => it.f_followeeId)),
this.followRequestsRepository.createQueryBuilder('f')
.select('f.followerId')
.where('f.followeeId = :me', { me })
.getRawMany<{ f_followerId: string }>()
.then(it => it.map(it => it.f_followerId)),
this.blockingsRepository.createQueryBuilder('b')
.select('b.blockeeId')
.where('b.blockerId = :me', { me })
.getRawMany<{ b_blockeeId: string }>()
.then(it => it.map(it => it.b_blockeeId)),
this.blockingsRepository.createQueryBuilder('b')
.select('b.blockerId')
.where('b.blockeeId = :me', { me })
.getRawMany<{ b_blockerId: string }>()
.then(it => it.map(it => it.b_blockerId)),
this.mutingsRepository.createQueryBuilder('m')
.select('m.muteeId')
.where('m.muterId = :me', { me })
.getRawMany<{ m_muteeId: string }>()
.then(it => it.map(it => it.m_muteeId)),
this.renoteMutingsRepository.createQueryBuilder('m')
.select('m.muteeId')
.where('m.muterId = :me', { me })
.getRawMany<{ m_muteeId: string }>()
.then(it => it.map(it => it.m_muteeId)),
]);
return new Map(
@ -682,18 +703,17 @@ export class UserEntityService implements OnModuleInit {
}
const _userIds = _users.map(u => u.id);
// -- 特に前提条件のない値群を取得
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
let profilesMap: Map<MiUser['id'], MiUserProfile> = new Map();
let userRelations: Map<MiUser['id'], UserRelation> = new Map();
let userMemos: Map<MiUser['id'], string | null> = new Map();
let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
if (options?.schema !== 'UserLite') {
profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
const meId = me ? me.id : null;
if (meId) {
userMemos = await this.userMemosRepository.findBy({ userId: meId })