Skip to content

Commit

Permalink
Merge pull request #203 from taiyme/merge-upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
taiyme authored Apr 28, 2024
2 parents 5b31fea + fd55f35 commit 15ca917
Show file tree
Hide file tree
Showing 21 changed files with 185 additions and 71 deletions.
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

0 comments on commit 15ca917

Please sign in to comment.