Merge remote-tracking branch 'refs/remotes/misskey-original/develop' into develop
# Conflicts: # packages/frontend/src/components/MkAbuseReport.vue # packages/frontend/src/components/MkMention.vue # packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
This commit is contained in:
commit
1a8d91355d
29 changed files with 458 additions and 108 deletions
|
|
@ -20,7 +20,7 @@ import type { Serialized } from '@/types.js';
|
|||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService implements OnApplicationShutdown {
|
||||
|
|
|
|||
|
|
@ -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,8 +459,8 @@ export class MfmService {
|
|||
},
|
||||
};
|
||||
|
||||
appendChildren(nodes, doc.body);
|
||||
appendChildren(nodes, body);
|
||||
|
||||
return `<p>${doc.body.innerHTML}</p>`;
|
||||
return new XMLSerializer().serializeToString(body);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { checkHttps } from '@/misc/check-https.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 {
|
||||
|
|
@ -39,7 +39,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');
|
||||
|
|
@ -47,16 +47,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}`);
|
||||
|
|
@ -86,12 +88,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);
|
||||
|
|
|
|||
|
|
@ -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; // Noteがsensitiveなら添付もsensitiveにする
|
||||
const file = await this.apImageService.resolveImage(actor, attach);
|
||||
if (file) files.push(file);
|
||||
}
|
||||
|
||||
// リプライ
|
||||
const reply: MiNote | null = note.inReplyTo
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface IObject {
|
|||
endTime?: Date;
|
||||
icon?: any;
|
||||
image?: any;
|
||||
mediaType?: string;
|
||||
url?: ApObject | string;
|
||||
href?: string;
|
||||
tag?: IObject | IObject[];
|
||||
|
|
@ -241,14 +242,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 {
|
||||
|
|
|
|||
|
|
@ -120,12 +120,20 @@ export class ServerService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const name = path.split('@')[0].replace(/\.webp$/i, '');
|
||||
const host = path.split('@')[1]?.replace(/\.webp$/i, '');
|
||||
const emojiPath = path.replace(/\.webp$/i, '');
|
||||
const pathChunks = emojiPath.split('@');
|
||||
|
||||
if (pathChunks.length > 2) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = pathChunks.shift();
|
||||
const host = pathChunks.pop();
|
||||
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
// `@.` is the spec of ReactionService.decodeReaction
|
||||
host: (host == null || host === '.') ? IsNull() : host,
|
||||
host: (host === undefined || host === '.') ? IsNull() : host,
|
||||
name: name,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,188 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
image: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
link: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false,
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
paginationLinks: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
self: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
first: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
next: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
last: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
prev: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
link: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
optional: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
guid: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
pubDate: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
creator: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
isoDate: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
categories: {
|
||||
type: 'array',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
contentSnippet: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
enclosure: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false,
|
||||
},
|
||||
length: {
|
||||
type: 'number',
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
feedUrl: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
itunes: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
image: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
explicit: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
categories: {
|
||||
type: 'array',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
keywords: {
|
||||
type: 'array',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,10 @@ export class ClientServerService {
|
|||
// %71ueueとかでリクエストされたら困るため
|
||||
const url = decodeURI(request.routeOptions.url);
|
||||
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
|
||||
if (!url.startsWith(bullBoardPath + '/static/')) {
|
||||
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||
}
|
||||
|
||||
const token = request.cookies.token;
|
||||
if (token == null) {
|
||||
reply.code(401).send('Login required');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ extends ./base
|
|||
block vars
|
||||
- const user = page.user;
|
||||
- const title = page.title;
|
||||
- const url = `${config.url}/@${user.username}/${page.name}`;
|
||||
- const url = `${config.url}/@${user.username}/pages/${page.name}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ describe('MfmService', () => {
|
|||
const output = '<p>foo <i>bar</i></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('escape', () => {
|
||||
const input = '```\n<p>Hello, world!</p>\n```';
|
||||
const output = '<p><pre><code><p>Hello, world!</p></code></pre></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromHtml', () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,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, IPost } from '@/core/activitypub/type.js';
|
||||
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
|
||||
import { MiMeta, MiNote } from '@/models/_.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
|
|
@ -295,7 +295,7 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
imageObject,
|
||||
);
|
||||
assert.ok(!driveFile.isLink);
|
||||
assert.ok(driveFile && !driveFile.isLink);
|
||||
|
||||
const sensitiveImageObject: IApDocument = {
|
||||
type: 'Document',
|
||||
|
|
@ -308,7 +308,7 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
sensitiveImageObject,
|
||||
);
|
||||
assert.ok(!sensitiveDriveFile.isLink);
|
||||
assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink);
|
||||
});
|
||||
|
||||
test('cacheRemoteFiles=false disables caching', async () => {
|
||||
|
|
@ -324,7 +324,7 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
imageObject,
|
||||
);
|
||||
assert.ok(driveFile.isLink);
|
||||
assert.ok(driveFile && driveFile.isLink);
|
||||
|
||||
const sensitiveImageObject: IApDocument = {
|
||||
type: 'Document',
|
||||
|
|
@ -337,7 +337,7 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
sensitiveImageObject,
|
||||
);
|
||||
assert.ok(sensitiveDriveFile.isLink);
|
||||
assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
|
||||
});
|
||||
|
||||
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
|
||||
|
|
@ -353,7 +353,7 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
imageObject,
|
||||
);
|
||||
assert.ok(!driveFile.isLink);
|
||||
assert.ok(driveFile && !driveFile.isLink);
|
||||
|
||||
const sensitiveImageObject: IApDocument = {
|
||||
type: 'Document',
|
||||
|
|
@ -366,7 +366,19 @@ describe('ActivityPub', () => {
|
|||
await createRandomRemoteUser(resolver, personService),
|
||||
sensitiveImageObject,
|
||||
);
|
||||
assert.ok(sensitiveDriveFile.isLink);
|
||||
assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
|
||||
});
|
||||
|
||||
test('Link is not an attachment files', async () => {
|
||||
const linkObject: IObject = {
|
||||
type: 'Link',
|
||||
href: 'https://example.com/',
|
||||
};
|
||||
const driveFile = await imageService.createImage(
|
||||
await createRandomRemoteUser(resolver, personService),
|
||||
linkObject,
|
||||
);
|
||||
assert.strictEqual(driveFile, null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue