Compare commits
13 Commits
682b3451c7
...
8704d8830b
Author | SHA1 | Date |
---|---|---|
|
8704d8830b | |
|
4d562e7439 | |
|
30df768d26 | |
|
32cf70eb9b | |
|
d639f4bd4a | |
|
85b8af4b98 | |
|
2adf917fcf | |
|
fe8f20779b | |
|
ca30df4b90 | |
|
cc1fdece65 | |
|
9a71514b4c | |
|
fbf7c61c07 | |
|
1d2f2eda30 |
|
@ -9,6 +9,7 @@
|
|||
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
|
||||
- Enhance: クライアントエラー画面の多言語対応
|
||||
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
|
||||
- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 )
|
||||
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
|
||||
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"modern-ahocorasick": "2.0.3",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.26.0",
|
||||
|
|
|
@ -25,6 +25,7 @@ import { mainRouter } from '@/router/main.js';
|
|||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import type { Keymap } from '@/scripts/hotkey.js';
|
||||
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
|
||||
import { initWordMuteInfo } from '@/scripts/check-word-mute.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => {
|
||||
|
@ -344,11 +345,14 @@ export async function mainBoot() {
|
|||
}
|
||||
}
|
||||
|
||||
initWordMuteInfo();
|
||||
|
||||
const main = markRaw(stream.useChannel('main', null, 'System'));
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
updateAccountPartial(i);
|
||||
initWordMuteInfo();
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
|
|
|
@ -217,10 +217,9 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
const details: MenuItem[] = [];
|
||||
if ($i?.id === props.audio.userId) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
@ -228,6 +227,19 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if (iAmModerator) {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.moderation,
|
||||
icon: 'ti ti-photo-exclamation',
|
||||
to: `/admin/file/${props.audio.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
menu.push({ type: 'divider' }, ...details);
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
|
|
|
@ -133,10 +133,9 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
const details: MenuItem[] = [];
|
||||
if ($i?.id === props.image.userId) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
@ -144,6 +143,19 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if (iAmModerator) {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.moderation,
|
||||
icon: 'ti ti-photo-exclamation',
|
||||
to: `/admin/file/${props.image.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
menuItems.push({ type: 'divider' }, ...details);
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
|
|
|
@ -242,10 +242,9 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
const details: MenuItem[] = [];
|
||||
if ($i?.id === props.video.userId) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
@ -253,6 +252,19 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if (iAmModerator) {
|
||||
details.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.moderation,
|
||||
icon: 'ti ti-photo-exclamation',
|
||||
to: `/admin/file/${props.video.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
menu.push({ type: 'divider' }, ...details);
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
|
|
|
@ -282,8 +282,8 @@ const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filte
|
|||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||
const collapsed = ref(appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
const muted = ref(checkMute(appearNote.value, 'soft'));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, 'hard', true));
|
||||
const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
|
@ -302,20 +302,18 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
}));
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
|
||||
*/
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
||||
if (mutedWords != null) {
|
||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
||||
if (Array.isArray(result)) return result;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
||||
const result = checkWordMute(noteToCheck, $i, type);
|
||||
if (Array.isArray(result)) return result;
|
||||
|
||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
||||
if (Array.isArray(replyResult)) return replyResult;
|
||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, type);
|
||||
if (Array.isArray(replyResult)) return replyResult;
|
||||
|
||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
||||
if (Array.isArray(renoteResult)) return renoteResult;
|
||||
}
|
||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, type);
|
||||
if (Array.isArray(renoteResult)) return renoteResult;
|
||||
|
||||
if (checkOnly) return false;
|
||||
|
||||
|
|
|
@ -298,7 +298,7 @@ const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
|
||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, 'soft') : false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||
|
|
|
@ -62,7 +62,7 @@ const props = withDefaults(defineProps<{
|
|||
depth: 1,
|
||||
});
|
||||
|
||||
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
const muted = ref($i ? checkWordMute(props.note, $i, 'soft') : false);
|
||||
|
||||
const showContent = ref(false);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
|
|
@ -109,6 +109,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<a style="display: inline-block;" class="pepabo" title="GMO Pepabo" href="https://pepabo.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/gmo_pepabo.svg" alt="GMO Pepabo"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="purpledotdigital" title="Purple Dot Digital" href="https://purpledotdigital.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/purple-dot-digital.jpg" alt="Purple Dot Digital"></a>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
|
|
|
@ -2,42 +2,101 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as AhoCorasick from 'modern-ahocorasick';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
type WordMuteInfo = false | {
|
||||
normals: string[];
|
||||
and: string[][];
|
||||
regex: Array<{ original: string; regex: RegExp }>;
|
||||
ahoCorasick: AhoCorasick.default;
|
||||
}
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
type GlobalMisskeyWordMute = {
|
||||
soft: WordMuteInfo;
|
||||
hard: WordMuteInfo;
|
||||
};
|
||||
|
||||
if (text === '') return false;
|
||||
export function createWordMuteInfo(mutedWords: Array<string | string[]>) : WordMuteInfo {
|
||||
if (mutedWords.length <= 0) return false;
|
||||
const normalTexts: string[] = [];
|
||||
const andTexts: string[][] = [];
|
||||
const regexTexts: Array<{ original: string; regex: RegExp }> = [];
|
||||
|
||||
const matched = mutedWords.filter(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
// Clean up
|
||||
const filteredFilter = filter.filter(keyword => keyword !== '');
|
||||
if (filteredFilter.length === 0) return false;
|
||||
|
||||
return filteredFilter.every(keyword => text.includes(keyword));
|
||||
for (const filter of mutedWords) {
|
||||
if (Array.isArray(filter)) {
|
||||
if (filter.length === 1) {
|
||||
normalTexts.push(filter[0]);
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
||||
// This should never happen due to input sanitisation.
|
||||
if (!regexp) return false;
|
||||
|
||||
try {
|
||||
return new RegExp(regexp[1], regexp[2]).test(text);
|
||||
} catch (err) {
|
||||
// This should never happen due to input sanitisation.
|
||||
return false;
|
||||
}
|
||||
andTexts.push(filter);
|
||||
}
|
||||
});
|
||||
|
||||
if (matched.length > 0) return matched;
|
||||
} else if (filter.startsWith('/') && filter.endsWith('/')) {
|
||||
const regExp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
if (!regExp) continue;
|
||||
try {
|
||||
regexTexts.push({ original: filter, regex: new RegExp(filter.slice(1, -1)) });
|
||||
} catch {
|
||||
// 無効な正規表現はスキップ
|
||||
}
|
||||
} else {
|
||||
normalTexts.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
const ac = new AhoCorasick.default(normalTexts);
|
||||
|
||||
return {
|
||||
normals: normalTexts,
|
||||
and: andTexts,
|
||||
regex: regexTexts,
|
||||
ahoCorasick: ac,
|
||||
};
|
||||
}
|
||||
|
||||
function setWordMuteInfo(mutedWords: Array<string | string[]>, hardMutedWords: Array<string | string[]>): void {
|
||||
const soft = createWordMuteInfo(mutedWords);
|
||||
const hard = createWordMuteInfo(hardMutedWords);
|
||||
|
||||
globalThis._misskeyWordMute = { soft, hard };
|
||||
}
|
||||
|
||||
function getWordMuteInfo(): GlobalMisskeyWordMute | undefined {
|
||||
if (!globalThis._misskeyWordMute) return undefined;
|
||||
return globalThis._misskeyWordMute as unknown as GlobalMisskeyWordMute;
|
||||
}
|
||||
|
||||
export function initWordMuteInfo(): void {
|
||||
const mutedWords = $i?.mutedWords ?? [];
|
||||
const hardMutedWords = $i?.hardMutedWords ?? [];
|
||||
|
||||
setWordMuteInfo(mutedWords, hardMutedWords);
|
||||
}
|
||||
|
||||
export function checkWordMute(
|
||||
note: Misskey.entities.Note,
|
||||
me: Misskey.entities.UserLite | null | undefined,
|
||||
type: 'soft' | 'hard',
|
||||
): Array<string | string[]> | false {
|
||||
// 自分自身の投稿は対象外
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
const wordMuteInfo = getWordMuteInfo()?.[type];
|
||||
|
||||
if (wordMuteInfo == null || wordMuteInfo === false) return false;
|
||||
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
if (text === '') return false;
|
||||
|
||||
const normalMatches = wordMuteInfo.ahoCorasick.search(text);
|
||||
|
||||
// andTexts
|
||||
const andMatches = wordMuteInfo.and.filter(texts => texts.filter(keyword => keyword !== '').every(keyword => text.includes(keyword)));
|
||||
|
||||
// RegExp
|
||||
const regexMatches = wordMuteInfo.regex.filter(({ regex }) => regex.test(text));
|
||||
|
||||
const matched: Array<string | string[]> = normalMatches.map(match => match[1]).concat(andMatches, regexMatches.map(({ original }) => original));
|
||||
|
||||
return matched.length > 0 ? matched : false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export const UserLiteMock = vi.fn(() => {
|
||||
return {
|
||||
id: 'xxxxxxxx',
|
||||
username: 'ai',
|
||||
host: null,
|
||||
name: '藍',
|
||||
avatarUrl: null,
|
||||
avatarBlurhash: null,
|
||||
avatarDecorations: [],
|
||||
emojis: {},
|
||||
onlineStatus: 'online',
|
||||
} as Misskey.entities.UserLite;
|
||||
});
|
||||
|
||||
export const NoteMock = vi.fn((options?: {
|
||||
text?: string,
|
||||
cw?: string,
|
||||
}) => {
|
||||
const user = new UserLiteMock();
|
||||
return {
|
||||
id: 'xxxxxxxx',
|
||||
// 2025/01/01 00:00:00 UTC on Unix time
|
||||
createdAt: '1767225600000',
|
||||
text: options?.text ?? 'Hello, Misskey!',
|
||||
cw: options?.cw,
|
||||
userId: user.id,
|
||||
user: user,
|
||||
visibility: 'public',
|
||||
reactionAcceptance: null,
|
||||
reactionEmojis: {},
|
||||
reactions: {},
|
||||
reactionCount: 0,
|
||||
renoteCount: 0,
|
||||
repliesCount: 0,
|
||||
} as Misskey.entities.Note;
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, test, assert } from 'vitest';
|
||||
import { createWordMuteInfo, checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { NoteMock } from './mocks.js';
|
||||
|
||||
type TestCases = {
|
||||
text: string,
|
||||
cw?: string,
|
||||
mutedWords: Array<string | string[]>,
|
||||
result: false | Array<string | string[]>,
|
||||
}
|
||||
|
||||
describe('check-word-mute', () => {
|
||||
const cases:Array<TestCases> = [
|
||||
{
|
||||
text: 'Hello, Misskey!',
|
||||
mutedWords: ['Misskey'],
|
||||
result: [['Misskey']],
|
||||
},
|
||||
// cw
|
||||
{
|
||||
text: 'Hello, Misskey!',
|
||||
cw: 'ai',
|
||||
mutedWords: ['ai'],
|
||||
result: [['ai']],
|
||||
},
|
||||
// nothing
|
||||
{
|
||||
text: 'Hello, Misskey!',
|
||||
mutedWords: [],
|
||||
result: false,
|
||||
},
|
||||
// surrogate pair
|
||||
{
|
||||
text: '𠮟る',
|
||||
mutedWords: ['𠮟'],
|
||||
result: [['𠮟']],
|
||||
},
|
||||
// grapheme cluster
|
||||
{
|
||||
text: '👩❤️👨',
|
||||
mutedWords: ['👩'],
|
||||
result: false,
|
||||
},
|
||||
// regex
|
||||
{
|
||||
text: 'Hello, Misskey!',
|
||||
mutedWords: ['/M[isk]*ey/'],
|
||||
result: ['/M[isk]*ey/'],
|
||||
},
|
||||
// multi words
|
||||
{
|
||||
text: 'Hello, Misskey!',
|
||||
mutedWords: [['Hello', 'Misskey'], ['Mi']],
|
||||
result: [['Mi'],['Hello', 'Misskey']],
|
||||
},
|
||||
]
|
||||
|
||||
cases.forEach((c) => {
|
||||
test(`text: ${c.text}, cw: ${c.cw}, mutedWords: ${c.mutedWords}` , async () => {
|
||||
// initWordMuteInfoが実行されないので代わりにここで初期化
|
||||
(globalThis as any)._misskeyWordMute = {
|
||||
soft: createWordMuteInfo(c.mutedWords),
|
||||
hard: createWordMuteInfo([]),
|
||||
}
|
||||
|
||||
const note = NoteMock({ text: c.text, cw: c.cw });
|
||||
const result = checkWordMute(note, null, 'soft');
|
||||
assert.deepEqual(result, c.result);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -799,6 +799,9 @@ importers:
|
|||
misskey-reversi:
|
||||
specifier: workspace:*
|
||||
version: link:../misskey-reversi
|
||||
modern-ahocorasick:
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3
|
||||
photoswipe:
|
||||
specifier: 5.4.4
|
||||
version: 5.4.4
|
||||
|
@ -7854,6 +7857,7 @@ packages:
|
|||
|
||||
lodash.get@4.4.2:
|
||||
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
|
||||
|
||||
lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
|
@ -8311,6 +8315,9 @@ packages:
|
|||
resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
modern-ahocorasick@2.0.3:
|
||||
resolution: {integrity: sha512-3vsbnf5DmpsaE8Ye892HecJU7kaT2svsIBXNhne1J080WlU9RKjTtE5PgX+OCc2huqGqGYO+rVEsJlJJuQj+Qw==}
|
||||
|
||||
module-details-from-path@1.0.3:
|
||||
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
|
||||
|
||||
|
@ -19896,6 +19903,8 @@ snapshots:
|
|||
|
||||
mock-socket@9.3.1: {}
|
||||
|
||||
modern-ahocorasick@2.0.3: {}
|
||||
|
||||
module-details-from-path@1.0.3: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
|
Loading…
Reference in New Issue