Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge upstream #203

Merged
merged 12 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
- 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
- Enhance: フォローするかどうかの確認ダイアログを出せるように
- Enhance: Playを手動でリロードできるように
- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように
- Chore: AiScriptを0.18.0にバージョンアップ
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
Expand All @@ -50,6 +52,10 @@
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正
- Fix: ページのOGP URLが間違っているのを修正
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正

### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
Expand All @@ -62,10 +68,12 @@
- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正
- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606)
- Fix: Add Cache-Control to Bull Board
- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正
- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正
- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正
- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正
- Fix: AP Link等は添付ファイル扱いしないようになど (#13754)

## 2024.3.1

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/CustomEmojiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';

const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;

@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
Expand Down
8 changes: 5 additions & 3 deletions packages/backend/src/core/MfmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
19 changes: 10 additions & 9 deletions packages/backend/src/core/activitypub/models/ApImageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -39,24 +39,26 @@ 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');
}

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}`);
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 7 additions & 10 deletions packages/backend/src/core/activitypub/models/ApNoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from '@/models/_.js';
Expand Down Expand Up @@ -209,15 +208,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
Expand Down
11 changes: 6 additions & 5 deletions packages/backend/src/core/activitypub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface IObject {
endTime?: Date;
icon?: any;
image?: any;
mediaType?: string;
url?: ApObject | string;
href?: string;
tag?: IObject | IObject[];
Expand Down Expand Up @@ -240,14 +241,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 {
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/web/ClientServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,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');
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/web/views/page.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/test/unit/MfmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
});

describe('fromHtml', () => {
Expand Down
26 changes: 19 additions & 7 deletions packages/backend/test/unit/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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 () => {
Expand All @@ -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',
Expand All @@ -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 () => {
Expand All @@ -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',
Expand All @@ -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);
});
});
});
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkAbuseReport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="detail">
<div>
<Mfm :text="report.comment"/>
<Mfm :text="report.comment" :linkBehavior="'window'"/>
</div>
<hr/>
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/components/MkLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:rel="rel ?? 'nofollow noopener'"
:target="target"
:title="url"
:behavior="behavior"
>
<slot></slot>
<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
Expand All @@ -25,11 +26,12 @@ import { url as local } from '@/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/instance.js';
import MkA from '@/components/global/MkA.vue';
import MkA, { type MkABehavior } from '@/components/global/MkA.vue';

const props = withDefaults(defineProps<{
url: string;
rel?: null | string;
behavior?: MkABehavior;
}>(), {
});

Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/components/MkMention.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="behavior">
<img :class="$style.icon" :src="avatarUrl" alt="">
<span>
<span>@{{ username }}</span>
Expand All @@ -21,10 +21,12 @@ import { host as localHost } from '@/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { type MkABehavior } from '@/components/global/MkA.vue';

const props = defineProps<{
username: string;
host: string;
behavior?: MkABehavior;
}>();

const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
Expand Down
11 changes: 8 additions & 3 deletions packages/frontend/src/components/MkNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
Expand All @@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote?.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
Expand Down Expand Up @@ -174,6 +174,11 @@ const rejectFollowRequest = () => {
followRequestDone.value = true;
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
};

function getActualReactedUsersCount(notification: Misskey.entities.Notification) {
if (notification.type !== 'reaction:grouped') return 0;
return new Set(notification.reactions.map((reaction) => reaction.user.id)).size;
}
</script>

<style lang="scss" module>
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/components/global/MkA.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</a>
</template>

<script lang="ts">
export type MkABehavior = 'window' | 'browser' | null;
</script>

<script lang="ts" setup>
import { computed, shallowRef } from 'vue';
import * as os from '@/os.js';
Expand All @@ -26,7 +30,7 @@ import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{
to: string;
activeClass?: null | string;
behavior?: null | 'window' | 'browser';
behavior?: MkABehavior;
}>(), {
activeClass: null,
behavior: null,
Expand Down
Loading