Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d8434a206f | ||
![]() |
83159600ea | ||
![]() |
12b82aca5f | ||
![]() |
73842166ee | ||
![]() |
30594dde18 | ||
![]() |
7948018e6a | ||
![]() |
8fb8d7c10c | ||
![]() |
7ca0af9e7e | ||
![]() |
ac2bace764 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -12,11 +12,20 @@
|
||||
|
||||
-->
|
||||
|
||||
## 202x.x.x (Unreleased)
|
||||
|
||||
### Client
|
||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||
- Enhance: ActivityPubをサポートしているウェブリンクを展開できるように
|
||||
|
||||
## 2023.12.2
|
||||
|
||||
### General
|
||||
- v2023.12.1でDockerを利用してサーバーを起動できない問題を修正
|
||||
|
||||
### Client
|
||||
- Enhance: 検索画面においてEnterキー押下で検索できるように
|
||||
|
||||
## 2023.12.1
|
||||
|
||||
### Note
|
||||
@@ -124,7 +133,6 @@
|
||||
- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように
|
||||
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
|
||||
- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
|
||||
- Enhance: 検索画面においてEnterキー押下で検索できるように
|
||||
|
||||
### Server
|
||||
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
||||
|
2
COPYING
2
COPYING
@@ -1,5 +1,5 @@
|
||||
Unless otherwise stated this repository is
|
||||
Copyright © 2014-2023 syuilo and contributers
|
||||
Copyright © 2014-2023 syuilo and contributors
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
@@ -609,6 +609,7 @@ export interface Locale {
|
||||
"enablePlayer": string;
|
||||
"disablePlayer": string;
|
||||
"expandTweet": string;
|
||||
"expandNote": string;
|
||||
"themeEditor": string;
|
||||
"description": string;
|
||||
"describeFile": string;
|
||||
|
@@ -606,6 +606,7 @@ useCw: "内容を隠す"
|
||||
enablePlayer: "プレイヤーを開く"
|
||||
disablePlayer: "プレイヤーを閉じる"
|
||||
expandTweet: "ポストを展開する"
|
||||
expandNote: "ノートを展開する"
|
||||
themeEditor: "テーマエディター"
|
||||
description: "説明"
|
||||
describeFile: "キャプションを付ける"
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"build-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build",
|
||||
"build-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"init": "pnpm migrate",
|
||||
|
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SupportTrueMailApi1703658526000 {
|
||||
name = 'SupportTrueMailApi1703658526000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`);
|
||||
}
|
||||
}
|
@@ -156,7 +156,7 @@ export class EmailService {
|
||||
@bindThis
|
||||
public async validateEmailForAccount(emailAddress: string): Promise<{
|
||||
available: boolean;
|
||||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned';
|
||||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
|
||||
}> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
@@ -173,6 +173,8 @@ export class EmailService {
|
||||
if (meta.enableActiveEmailValidation) {
|
||||
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
||||
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
||||
} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
|
||||
} else {
|
||||
validated = await validateEmail({
|
||||
email: emailAddress,
|
||||
@@ -201,6 +203,8 @@ export class EmailService {
|
||||
validated.reason === 'disposable' ? 'disposable' :
|
||||
validated.reason === 'mx' ? 'mx' :
|
||||
validated.reason === 'smtp' ? 'smtp' :
|
||||
validated.reason === 'network' ? 'network' :
|
||||
validated.reason === 'blacklist' ? 'blacklist' :
|
||||
null,
|
||||
};
|
||||
}
|
||||
@@ -265,4 +269,67 @@ export class EmailService {
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async trueMail<T>(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{
|
||||
valid: boolean;
|
||||
reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null;
|
||||
}> {
|
||||
const endpoint = truemailInstance + '?email=' + emailAddress;
|
||||
try {
|
||||
const res = await this.httpRequestService.send(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: truemailAuthKey
|
||||
},
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
email: string;
|
||||
success: boolean;
|
||||
errors?: {
|
||||
list_match?: string;
|
||||
regex?: string;
|
||||
mx?: string;
|
||||
smtp?: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
if (json.email === undefined || (json.email !== undefined && json.errors?.regex)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'format',
|
||||
};
|
||||
}
|
||||
if (json.errors?.smtp) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'smtp',
|
||||
};
|
||||
}
|
||||
if (json.errors?.mx) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'mx',
|
||||
};
|
||||
}
|
||||
if (!json.success) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: json.errors?.list_match as T || 'blacklist',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
reason: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'network',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -457,6 +457,23 @@ export class MiMeta {
|
||||
})
|
||||
public verifymailAuthKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableTruemailApi: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public truemailInstance: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public truemailAuthKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
|
@@ -284,6 +284,18 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableTruemailApi: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
truemailInstance: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
truemailAuthKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableChartsForRemoteUser: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -520,6 +532,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
enableVerifymailApi: instance.enableVerifymailApi,
|
||||
verifymailAuthKey: instance.verifymailAuthKey,
|
||||
enableTruemailApi: instance.enableTruemailApi,
|
||||
truemailInstance: instance.truemailInstance,
|
||||
truemailAuthKey: instance.truemailAuthKey,
|
||||
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
|
@@ -116,6 +116,9 @@ export const paramDef = {
|
||||
enableActiveEmailValidation: { type: 'boolean' },
|
||||
enableVerifymailApi: { type: 'boolean' },
|
||||
verifymailAuthKey: { type: 'string', nullable: true },
|
||||
enableTruemailApi: { type: 'boolean' },
|
||||
truemailInstance: { type: 'string', nullable: true },
|
||||
truemailAuthKey: { type: 'string', nullable: true },
|
||||
enableChartsForRemoteUser: { type: 'boolean' },
|
||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||
enableServerMachineStats: { type: 'boolean' },
|
||||
@@ -470,6 +473,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.enableTruemailApi !== undefined) {
|
||||
set.enableTruemailApi = ps.enableTruemailApi;
|
||||
}
|
||||
|
||||
if (ps.truemailInstance !== undefined) {
|
||||
if (ps.truemailInstance === '') {
|
||||
set.truemailInstance = null;
|
||||
} else {
|
||||
set.truemailInstance = ps.truemailInstance;
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.truemailAuthKey !== undefined) {
|
||||
if (ps.truemailAuthKey === '') {
|
||||
set.truemailAuthKey = null;
|
||||
} else {
|
||||
set.truemailAuthKey = ps.truemailAuthKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.enableChartsForRemoteUser !== undefined) {
|
||||
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<MkNoteSimple v-if="appearNote.renote" :class="$style.quote" :note="appearNote.renote" :quoted="true"/>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
@@ -801,14 +801,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
}
|
||||
|
||||
.quote {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.quoteNote {
|
||||
padding: 16px;
|
||||
border: dashed 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.channel {
|
||||
@@ -947,12 +940,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 250px) {
|
||||
.quoteNote {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="[$style.root, quoted ? $style.quoted : null]">
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
@@ -30,6 +30,8 @@ import MkCwButton from '@/components/MkCwButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
quoted?: boolean;
|
||||
}>();
|
||||
|
||||
const showContent = ref(false);
|
||||
@@ -78,12 +80,23 @@ const showContent = ref(false);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.quoted {
|
||||
padding: 16px;
|
||||
border: dashed 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@container (min-width: 250px) {
|
||||
.avatar {
|
||||
margin: 0 10px 0 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.quoted {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 350px) {
|
||||
|
@@ -752,7 +752,17 @@ async function post(ev?: MouseEvent) {
|
||||
|
||||
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
|
||||
const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
|
||||
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
|
||||
if (!postData.text) {
|
||||
postData.text = hashtags_;
|
||||
} else {
|
||||
const postTextLines = postData.text.split('\n');
|
||||
if (postTextLines[postTextLines.length - 1].trim() === '') {
|
||||
postTextLines[postTextLines.length - 1] += hashtags_;
|
||||
} else {
|
||||
postTextLines[postTextLines.length - 1] += ' ' + hashtags_;
|
||||
}
|
||||
postData.text = postTextLines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// plugin
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<template v-if="player.url && playerEnabled">
|
||||
<div v-if="player.url && playerEnabled">
|
||||
<div
|
||||
:class="$style.player"
|
||||
:style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`"
|
||||
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tweetId && tweetExpanded">
|
||||
<div ref="twitter">
|
||||
</div>
|
||||
<div v-else-if="postExpanded">
|
||||
<div v-if="tweetId" ref="twitter">
|
||||
<iframe
|
||||
ref="tweet"
|
||||
allow="fullscreen;web-share"
|
||||
@@ -37,12 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
></iframe>
|
||||
</div>
|
||||
<MkNoteSimple v-else-if="note" :note="note" :quoted="true"/>
|
||||
<div :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = false">
|
||||
<i class="ti ti-x"></i> {{ i18n.ts.close }}
|
||||
<MkButton :small="true" inline @click="postExpanded = false">
|
||||
<i v-if="tweetId" class="ti ti-x"></i> {{ i18n.ts.close }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
|
||||
@@ -66,10 +67,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</component>
|
||||
<template v-if="showActions">
|
||||
<div v-if="tweetId" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = true">
|
||||
<MkButton :small="true" inline @click="postExpanded = true">
|
||||
<i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="noteUrl || note" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="resolveNote()">
|
||||
{{ i18n.ts.expandNote }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="!playerEnabled && player.url" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="playerEnabled = true">
|
||||
<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
|
||||
@@ -85,11 +91,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onUnmounted, ref } from 'vue';
|
||||
import type { summaly } from 'summaly';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { url as local } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
@@ -126,7 +134,9 @@ const player = ref({
|
||||
} as SummalyResult['player']);
|
||||
const playerEnabled = ref(false);
|
||||
const tweetId = ref<string | null>(null);
|
||||
const tweetExpanded = ref(props.detail);
|
||||
const noteUrl = ref<string | null>(null);
|
||||
const note = ref<Misskey.entities.Note | null>(null);
|
||||
const postExpanded = ref(props.detail);
|
||||
const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
|
||||
const tweetHeight = ref(150);
|
||||
const unknownUrl = ref(false);
|
||||
@@ -172,9 +182,40 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
|
||||
sitename.value = info.sitename;
|
||||
player.value = info.player;
|
||||
sensitive.value = info.sensitive ?? false;
|
||||
noteUrl.value = info.activityPub;
|
||||
if (postExpanded.value) {
|
||||
resolveNote();
|
||||
}
|
||||
});
|
||||
|
||||
function adjustTweetHeight(message: any) {
|
||||
async function resolveNote(): Promise<void> {
|
||||
if (note.value) {
|
||||
// Reuse the data
|
||||
postExpanded.value = true;
|
||||
return;
|
||||
}
|
||||
if (!noteUrl.value) {
|
||||
// Note does not exist
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fetching.value = true;
|
||||
const result = await os.api('ap/show', { uri: noteUrl.value });
|
||||
if (result.type === 'Note') {
|
||||
note.value = result.object;
|
||||
postExpanded.value = true;
|
||||
} else {
|
||||
postExpanded.value = false;
|
||||
}
|
||||
} finally {
|
||||
// Prevent repeated resolving
|
||||
noteUrl.value = null;
|
||||
fetching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function adjustTweetHeight(message: any): void {
|
||||
if (message.origin !== 'https://platform.twitter.com') return;
|
||||
const embed = message.data?.['twttr.embed'];
|
||||
if (embed?.method !== 'twttr.private.resize') return;
|
||||
|
@@ -80,6 +80,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Verifymail.io API Auth Key</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="enableTruemailApi" @update:modelValue="save">
|
||||
<template #label>Use TrueMail API</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="truemailInstance" @update:modelValue="save">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>TrueMail API Instance</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="truemailAuthKey" @update:modelValue="save">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>TrueMail API Auth Key</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
@@ -153,6 +164,9 @@ const enableIpLogging = ref<boolean>(false);
|
||||
const enableActiveEmailValidation = ref<boolean>(false);
|
||||
const enableVerifymailApi = ref<boolean>(false);
|
||||
const verifymailAuthKey = ref<string | null>(null);
|
||||
const enableTruemailApi = ref<boolean>(false);
|
||||
const truemailInstance = ref<string | null>(null);
|
||||
const truemailAuthKey = ref<string | null>(null);
|
||||
const bannedEmailDomains = ref<string>('');
|
||||
|
||||
async function init() {
|
||||
@@ -194,6 +208,9 @@ function save() {
|
||||
enableActiveEmailValidation: enableActiveEmailValidation.value,
|
||||
enableVerifymailApi: enableVerifymailApi.value,
|
||||
verifymailAuthKey: verifymailAuthKey.value,
|
||||
enableTruemailApi: enableTruemailApi.value,
|
||||
truemailInstance: truemailInstance.value,
|
||||
truemailAuthKey: truemailAuthKey.value,
|
||||
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
|
@@ -3,10 +3,11 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, test, assert, afterEach } from 'vitest';
|
||||
import { describe, test, assert, afterEach, beforeAll, vi } from 'vitest';
|
||||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type { summaly } from 'summaly';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { components } from '@/components/index.js';
|
||||
import { directives } from '@/directives/index.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
@@ -47,13 +48,18 @@ describe('MkUrlPreview', () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const renderAndOpenPreview = async (summary: Partial<SummalyResult>): Promise<HTMLIFrameElement | null> => {
|
||||
const renderAndOpenPreview = async (summary: Partial<SummalyResult>): Promise<RenderResult> => {
|
||||
const mkUrlPreview = await renderPreviewBy(summary);
|
||||
const buttons = mkUrlPreview.getAllByRole('button');
|
||||
buttons[0].click();
|
||||
// Wait for the click event to be fired
|
||||
await Promise.resolve();
|
||||
|
||||
return mkUrlPreview;
|
||||
};
|
||||
|
||||
const renderAndOpenPreviewInIFrame = async (summary: Partial<SummalyResult>): Promise<HTMLIFrameElement | null> => {
|
||||
const mkUrlPreview = await renderAndOpenPreview(summary);
|
||||
return mkUrlPreview.container.querySelector('iframe');
|
||||
};
|
||||
|
||||
@@ -85,7 +91,7 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Having a player should setup the iframe', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
@@ -103,7 +109,7 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Having a player with `allow` field should set permissions', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
@@ -117,7 +123,7 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Having a player width should keep the fixed aspect ratio', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
@@ -131,7 +137,7 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Having a player width should keep the fixed height', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
@@ -145,7 +151,7 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Loading a tweet in iframe', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://twitter.com/i/web/status/1685072521782325249',
|
||||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
@@ -154,11 +160,48 @@ describe('MkUrlPreview', () => {
|
||||
});
|
||||
|
||||
test('Loading a post in iframe', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
const iframe = await renderAndOpenPreviewInIFrame({
|
||||
url: 'https://x.com/i/web/status/1685072521782325249',
|
||||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
|
||||
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin');
|
||||
});
|
||||
|
||||
describe('ActivityPub notes', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Preview a note', async () => {
|
||||
vi.mock('@/os', () => {
|
||||
return {
|
||||
api(endpoint: string): unknown {
|
||||
if (endpoint === 'ap/show') {
|
||||
return {
|
||||
type: 'Note',
|
||||
object: {
|
||||
text: 'Mizuki',
|
||||
createdAt: new Date().toISOString(),
|
||||
user: {},
|
||||
files: [] as misskey.entities.DriveFile[],
|
||||
} as misskey.entities.Note,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected api call ${endpoint}`);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const url = 'https://example.local';
|
||||
const renderResult = await renderAndOpenPreview({
|
||||
url,
|
||||
description: 'Misskey',
|
||||
activityPub: url,
|
||||
});
|
||||
|
||||
assert.notExists(renderResult.queryByText('Misskey'), 'Original description should disappear');
|
||||
assert.exists(renderResult.queryByText('Mizuki'), 'ActivityPub fetch result should appear');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user