mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-24 23:15:13 +00:00
Merge branch 'develop' into hazelnoot/following-timeline
This commit is contained in:
commit
0da7f9b3f6
26 changed files with 542 additions and 41 deletions
|
@ -61,6 +61,9 @@ copyNoteId: "Copy note ID"
|
||||||
copyFileId: "Copy file ID"
|
copyFileId: "Copy file ID"
|
||||||
copyFolderId: "Copy folder ID"
|
copyFolderId: "Copy folder ID"
|
||||||
copyProfileUrl: "Copy profile URL"
|
copyProfileUrl: "Copy profile URL"
|
||||||
|
trustedLinkUrlPatterns: "Link to external site warning exclusion list"
|
||||||
|
trustedLinkUrlPatternsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition. Using surrounding keywords with slashes will turn them into a regular expression. If you write only the domain name, it will be a backward match."
|
||||||
|
open: "Open"
|
||||||
searchUser: "Search for a user"
|
searchUser: "Search for a user"
|
||||||
searchThisUsersNotes: "Search this user’s notes"
|
searchThisUsersNotes: "Search this user’s notes"
|
||||||
reply: "Reply"
|
reply: "Reply"
|
||||||
|
@ -152,6 +155,7 @@ renoteUnmute: "Unmute Boosts"
|
||||||
block: "Block"
|
block: "Block"
|
||||||
unblock: "Unblock"
|
unblock: "Unblock"
|
||||||
markAsNSFW: "Mark all media from user as NSFW"
|
markAsNSFW: "Mark all media from user as NSFW"
|
||||||
|
markInstanceAsNSFW: "Mark as NSFW"
|
||||||
suspend: "Suspend"
|
suspend: "Suspend"
|
||||||
unsuspend: "Unsuspend"
|
unsuspend: "Unsuspend"
|
||||||
blockConfirm: "Are you sure that you want to block this account?"
|
blockConfirm: "Are you sure that you want to block this account?"
|
||||||
|
@ -226,6 +230,7 @@ stopActivityDelivery: "Stop sending activities"
|
||||||
blockThisInstance: "Block this instance"
|
blockThisInstance: "Block this instance"
|
||||||
silenceThisInstance: "Silence this instance"
|
silenceThisInstance: "Silence this instance"
|
||||||
mediaSilenceThisInstance: "Silence media from this instance"
|
mediaSilenceThisInstance: "Silence media from this instance"
|
||||||
|
rejectReports: "Reject reports from this instance"
|
||||||
operations: "Operations"
|
operations: "Operations"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -266,6 +271,9 @@ noCustomEmojis: "There are no emoji"
|
||||||
noJobs: "There are no jobs"
|
noJobs: "There are no jobs"
|
||||||
federating: "Federating"
|
federating: "Federating"
|
||||||
blocked: "Blocked"
|
blocked: "Blocked"
|
||||||
|
blockedByBase: "This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s)."
|
||||||
|
silencedByBase: "This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s)."
|
||||||
|
mediaSilencedByBase: "This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s)."
|
||||||
suspended: "Suspended"
|
suspended: "Suspended"
|
||||||
all: "All"
|
all: "All"
|
||||||
subscribing: "Subscribing"
|
subscribing: "Subscribing"
|
||||||
|
@ -294,7 +302,6 @@ removeAreYouSure: "Are you sure that you want to remove \"{x}\"?"
|
||||||
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
|
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
|
||||||
resetAreYouSure: "Really reset?"
|
resetAreYouSure: "Really reset?"
|
||||||
areYouSure: "Are you sure?"
|
areYouSure: "Are you sure?"
|
||||||
confirmRemoteUrl: "Are you sure that you want to go to \"{x}\"?"
|
|
||||||
saved: "Saved"
|
saved: "Saved"
|
||||||
messaging: "Chat"
|
messaging: "Chat"
|
||||||
upload: "Upload"
|
upload: "Upload"
|
||||||
|
@ -624,6 +631,7 @@ unsetUserBanner: "Unset banner"
|
||||||
unsetUserBannerConfirm: "Are you sure you want to unset the banner?"
|
unsetUserBannerConfirm: "Are you sure you want to unset the banner?"
|
||||||
deleteAllFiles: "Delete all files"
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
|
deleteAllFilesQueued: "Deletion of all files queued"
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}. Please run this if the instance e.g. no longer exists."
|
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}. Please run this if the instance e.g. no longer exists."
|
||||||
userSuspended: "This user has been suspended."
|
userSuspended: "This user has been suspended."
|
||||||
|
@ -1334,6 +1342,10 @@ confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
|
||||||
sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?"
|
sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?"
|
||||||
createdLists: "Created lists"
|
createdLists: "Created lists"
|
||||||
createdAntennas: "Created antennas"
|
createdAntennas: "Created antennas"
|
||||||
|
severAllFollowRelations: "Break following relationships"
|
||||||
|
severAllFollowRelationsConfirm: "Really break all follow relationships? This is irreversible! This will break {followingCount} following and {followersCount} follower relations on {instanceName}!"
|
||||||
|
severAllFollowRelationsQueued: "Severing all follow relations with {host} queued."
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "Delivery status"
|
status: "Delivery status"
|
||||||
stop: "Suspend delivery"
|
stop: "Suspend delivery"
|
||||||
|
@ -2576,6 +2588,10 @@ _moderationLogTypes:
|
||||||
resetPassword: "Password reset"
|
resetPassword: "Password reset"
|
||||||
suspendRemoteInstance: "Remote instance suspended"
|
suspendRemoteInstance: "Remote instance suspended"
|
||||||
unsuspendRemoteInstance: "Remote instance unsuspended"
|
unsuspendRemoteInstance: "Remote instance unsuspended"
|
||||||
|
setRemoteInstanceNSFW: "Set remote instance as NSFW"
|
||||||
|
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
|
||||||
|
rejectRemoteInstanceReports: "Rejected reports from remote instance"
|
||||||
|
acceptRemoteInstanceReports: "Accepted reports from remote instance"
|
||||||
updateRemoteInstanceNote: "Moderation note updated for remote instance."
|
updateRemoteInstanceNote: "Moderation note updated for remote instance."
|
||||||
markSensitiveDriveFile: "File marked as sensitive"
|
markSensitiveDriveFile: "File marked as sensitive"
|
||||||
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
||||||
|
@ -2831,3 +2847,7 @@ _contextMenu:
|
||||||
app: "Application"
|
app: "Application"
|
||||||
appWithShift: "Application with shift key"
|
appWithShift: "Application with shift key"
|
||||||
native: "Native"
|
native: "Native"
|
||||||
|
_externalNavigationWarning:
|
||||||
|
title: "Navigate to an external site"
|
||||||
|
description: "Leave {host} and go to an external site"
|
||||||
|
trustThisDomain: "Trust this domain on this device in the future"
|
||||||
|
|
82
locales/index.d.ts
vendored
82
locales/index.d.ts
vendored
|
@ -260,6 +260,18 @@ export interface Locale extends ILocale {
|
||||||
* プロフィールURLをコピー
|
* プロフィールURLをコピー
|
||||||
*/
|
*/
|
||||||
"copyProfileUrl": string;
|
"copyProfileUrl": string;
|
||||||
|
/**
|
||||||
|
* 外部サイトへのリンク警告 除外リスト
|
||||||
|
*/
|
||||||
|
"trustedLinkUrlPatterns": string;
|
||||||
|
/**
|
||||||
|
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。
|
||||||
|
*/
|
||||||
|
"trustedLinkUrlPatternsDescription": string;
|
||||||
|
/**
|
||||||
|
* 開く
|
||||||
|
*/
|
||||||
|
"open": string;
|
||||||
/**
|
/**
|
||||||
* ユーザーを検索
|
* ユーザーを検索
|
||||||
*/
|
*/
|
||||||
|
@ -624,6 +636,10 @@ export interface Locale extends ILocale {
|
||||||
* ユーザーのすべてのメディアをNSFWとしてマークする
|
* ユーザーのすべてのメディアをNSFWとしてマークする
|
||||||
*/
|
*/
|
||||||
"markAsNSFW": string;
|
"markAsNSFW": string;
|
||||||
|
/**
|
||||||
|
* Mark as NSFW
|
||||||
|
*/
|
||||||
|
"markInstanceAsNSFW": string;
|
||||||
/**
|
/**
|
||||||
* 凍結
|
* 凍結
|
||||||
*/
|
*/
|
||||||
|
@ -920,6 +936,10 @@ export interface Locale extends ILocale {
|
||||||
* サーバーをメディアサイレンス
|
* サーバーをメディアサイレンス
|
||||||
*/
|
*/
|
||||||
"mediaSilenceThisInstance": string;
|
"mediaSilenceThisInstance": string;
|
||||||
|
/**
|
||||||
|
* Reject reports from this instance
|
||||||
|
*/
|
||||||
|
"rejectReports": string;
|
||||||
/**
|
/**
|
||||||
* 操作
|
* 操作
|
||||||
*/
|
*/
|
||||||
|
@ -1080,6 +1100,18 @@ export interface Locale extends ILocale {
|
||||||
* ブロック中
|
* ブロック中
|
||||||
*/
|
*/
|
||||||
"blocked": string;
|
"blocked": string;
|
||||||
|
/**
|
||||||
|
* This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s).
|
||||||
|
*/
|
||||||
|
"blockedByBase": string;
|
||||||
|
/**
|
||||||
|
* This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s).
|
||||||
|
*/
|
||||||
|
"silencedByBase": string;
|
||||||
|
/**
|
||||||
|
* This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s).
|
||||||
|
*/
|
||||||
|
"mediaSilencedByBase": string;
|
||||||
/**
|
/**
|
||||||
* 配信停止
|
* 配信停止
|
||||||
*/
|
*/
|
||||||
|
@ -1192,10 +1224,6 @@ export interface Locale extends ILocale {
|
||||||
* よろしいですか?
|
* よろしいですか?
|
||||||
*/
|
*/
|
||||||
"areYouSure": string;
|
"areYouSure": string;
|
||||||
/**
|
|
||||||
* 「{x}」を開きますか?
|
|
||||||
*/
|
|
||||||
"confirmRemoteUrl": ParameterizedString<"x">;
|
|
||||||
/**
|
/**
|
||||||
* 保存しました
|
* 保存しました
|
||||||
*/
|
*/
|
||||||
|
@ -2512,6 +2540,10 @@ export interface Locale extends ILocale {
|
||||||
* すべてのファイルを削除しますか?
|
* すべてのファイルを削除しますか?
|
||||||
*/
|
*/
|
||||||
"deleteAllFilesConfirm": string;
|
"deleteAllFilesConfirm": string;
|
||||||
|
/**
|
||||||
|
* キューに入れられたすべてのファイルの削除
|
||||||
|
*/
|
||||||
|
"deleteAllFilesQueued": string;
|
||||||
/**
|
/**
|
||||||
* フォローを全解除
|
* フォローを全解除
|
||||||
*/
|
*/
|
||||||
|
@ -5353,6 +5385,18 @@ export interface Locale extends ILocale {
|
||||||
* 作成したアンテナ
|
* 作成したアンテナ
|
||||||
*/
|
*/
|
||||||
"createdAntennas": string;
|
"createdAntennas": string;
|
||||||
|
/**
|
||||||
|
* 以下の関係をすべて断ち切る
|
||||||
|
*/
|
||||||
|
"severAllFollowRelations": string;
|
||||||
|
/**
|
||||||
|
* すべての人間関係を壊す?これは不可逆です!これは{instanceName}の{followingCount}フォローと{followersCount}フォロワーの関係を壊す!
|
||||||
|
*/
|
||||||
|
"severAllFollowRelationsConfirm": ParameterizedString<"instanceName" | "followingCount" | "followersCount">;
|
||||||
|
/**
|
||||||
|
* キューに入れられた{host}とのすべてのフォロー関係を切断する。
|
||||||
|
*/
|
||||||
|
"severAllFollowRelationsQueued": ParameterizedString<"host">;
|
||||||
"_delivery": {
|
"_delivery": {
|
||||||
/**
|
/**
|
||||||
* 配信状態
|
* 配信状態
|
||||||
|
@ -9988,6 +10032,22 @@ export interface Locale extends ILocale {
|
||||||
* リモートサーバーを再開
|
* リモートサーバーを再開
|
||||||
*/
|
*/
|
||||||
"unsuspendRemoteInstance": string;
|
"unsuspendRemoteInstance": string;
|
||||||
|
/**
|
||||||
|
* Set remote instance as NSFW
|
||||||
|
*/
|
||||||
|
"setRemoteInstanceNSFW": string;
|
||||||
|
/**
|
||||||
|
* Set remote instance as NSFW
|
||||||
|
*/
|
||||||
|
"unsetRemoteInstanceNSFW": string;
|
||||||
|
/**
|
||||||
|
* Rejected reports from remote instance
|
||||||
|
*/
|
||||||
|
"rejectRemoteInstanceReports": string;
|
||||||
|
/**
|
||||||
|
* Accepted reports from remote instance
|
||||||
|
*/
|
||||||
|
"acceptRemoteInstanceReports": string;
|
||||||
/**
|
/**
|
||||||
* リモートサーバーのモデレーションノート更新
|
* リモートサーバーのモデレーションノート更新
|
||||||
*/
|
*/
|
||||||
|
@ -10929,6 +10989,20 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"native": string;
|
"native": string;
|
||||||
};
|
};
|
||||||
|
"_externalNavigationWarning": {
|
||||||
|
/**
|
||||||
|
* 外部サイトに移動します
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* {host}を離れて外部サイトに移動します
|
||||||
|
*/
|
||||||
|
"description": ParameterizedString<"host">;
|
||||||
|
/**
|
||||||
|
* このデバイスで今後このドメインを信頼する
|
||||||
|
*/
|
||||||
|
"trustThisDomain": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -61,6 +61,9 @@ copyNoteId: "ノートIDをコピー"
|
||||||
copyFileId: "ファイルIDをコピー"
|
copyFileId: "ファイルIDをコピー"
|
||||||
copyFolderId: "フォルダーIDをコピー"
|
copyFolderId: "フォルダーIDをコピー"
|
||||||
copyProfileUrl: "プロフィールURLをコピー"
|
copyProfileUrl: "プロフィールURLをコピー"
|
||||||
|
trustedLinkUrlPatterns: "外部サイトへのリンク警告 除外リスト"
|
||||||
|
trustedLinkUrlPatternsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。"
|
||||||
|
open: "開く"
|
||||||
searchUser: "ユーザーを検索"
|
searchUser: "ユーザーを検索"
|
||||||
searchThisUsersNotes: "ユーザーのノートを検索"
|
searchThisUsersNotes: "ユーザーのノートを検索"
|
||||||
reply: "返信"
|
reply: "返信"
|
||||||
|
@ -152,6 +155,7 @@ renoteUnmute: "ブーストのミュートを解除"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
unblock: "ブロック解除"
|
unblock: "ブロック解除"
|
||||||
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
|
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
|
||||||
|
markInstanceAsNSFW: "Mark as NSFW"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
unsuspend: "解凍"
|
unsuspend: "解凍"
|
||||||
blockConfirm: "ブロックしますか?"
|
blockConfirm: "ブロックしますか?"
|
||||||
|
@ -226,6 +230,7 @@ stopActivityDelivery: "アクティビティの配送を停止"
|
||||||
blockThisInstance: "このサーバーをブロック"
|
blockThisInstance: "このサーバーをブロック"
|
||||||
silenceThisInstance: "サーバーをサイレンス"
|
silenceThisInstance: "サーバーをサイレンス"
|
||||||
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
||||||
|
rejectReports: "Reject reports from this instance"
|
||||||
operations: "操作"
|
operations: "操作"
|
||||||
software: "ソフトウェア"
|
software: "ソフトウェア"
|
||||||
version: "バージョン"
|
version: "バージョン"
|
||||||
|
@ -266,6 +271,9 @@ noCustomEmojis: "絵文字はありません"
|
||||||
noJobs: "ジョブはありません"
|
noJobs: "ジョブはありません"
|
||||||
federating: "連合中"
|
federating: "連合中"
|
||||||
blocked: "ブロック中"
|
blocked: "ブロック中"
|
||||||
|
blockedByBase: "This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s)."
|
||||||
|
silencedByBase: "This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s)."
|
||||||
|
mediaSilencedByBase: "This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s)."
|
||||||
suspended: "配信停止"
|
suspended: "配信停止"
|
||||||
all: "全て"
|
all: "全て"
|
||||||
subscribing: "購読中"
|
subscribing: "購読中"
|
||||||
|
@ -294,7 +302,6 @@ removeAreYouSure: "「{x}」を削除しますか?"
|
||||||
deleteAreYouSure: "「{x}」を削除しますか?"
|
deleteAreYouSure: "「{x}」を削除しますか?"
|
||||||
resetAreYouSure: "リセットしますか?"
|
resetAreYouSure: "リセットしますか?"
|
||||||
areYouSure: "よろしいですか?"
|
areYouSure: "よろしいですか?"
|
||||||
confirmRemoteUrl: "「{x}」を開きますか?"
|
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
messaging: "チャット"
|
messaging: "チャット"
|
||||||
upload: "アップロード"
|
upload: "アップロード"
|
||||||
|
@ -624,6 +631,7 @@ unsetUserBanner: "バナーを解除"
|
||||||
unsetUserBannerConfirm: "バナーを解除しますか?"
|
unsetUserBannerConfirm: "バナーを解除しますか?"
|
||||||
deleteAllFiles: "すべてのファイルを削除"
|
deleteAllFiles: "すべてのファイルを削除"
|
||||||
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
||||||
|
deleteAllFilesQueued: "キューに入れられたすべてのファイルの削除"
|
||||||
removeAllFollowing: "フォローを全解除"
|
removeAllFollowing: "フォローを全解除"
|
||||||
removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。"
|
removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。"
|
||||||
userSuspended: "このユーザーは凍結されています。"
|
userSuspended: "このユーザーは凍結されています。"
|
||||||
|
@ -1334,6 +1342,9 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
|
||||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||||
createdLists: "作成したリスト"
|
createdLists: "作成したリスト"
|
||||||
createdAntennas: "作成したアンテナ"
|
createdAntennas: "作成したアンテナ"
|
||||||
|
severAllFollowRelations: "以下の関係をすべて断ち切る"
|
||||||
|
severAllFollowRelationsConfirm: "すべての人間関係を壊す?これは不可逆です!これは{instanceName}の{followingCount}フォローと{followersCount}フォロワーの関係を壊す!"
|
||||||
|
severAllFollowRelationsQueued: "キューに入れられた{host}とのすべてのフォロー関係を切断する。"
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "配信状態"
|
status: "配信状態"
|
||||||
|
@ -2644,6 +2655,10 @@ _moderationLogTypes:
|
||||||
resetPassword: "パスワードをリセット"
|
resetPassword: "パスワードをリセット"
|
||||||
suspendRemoteInstance: "リモートサーバーを停止"
|
suspendRemoteInstance: "リモートサーバーを停止"
|
||||||
unsuspendRemoteInstance: "リモートサーバーを再開"
|
unsuspendRemoteInstance: "リモートサーバーを再開"
|
||||||
|
setRemoteInstanceNSFW: "Set remote instance as NSFW"
|
||||||
|
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
|
||||||
|
rejectRemoteInstanceReports: "Rejected reports from remote instance"
|
||||||
|
acceptRemoteInstanceReports: "Accepted reports from remote instance"
|
||||||
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
|
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
|
||||||
markSensitiveDriveFile: "ファイルをセンシティブ付与"
|
markSensitiveDriveFile: "ファイルをセンシティブ付与"
|
||||||
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
||||||
|
@ -2906,3 +2921,8 @@ _contextMenu:
|
||||||
app: "アプリケーション"
|
app: "アプリケーション"
|
||||||
appWithShift: "Shiftキーでアプリケーション"
|
appWithShift: "Shiftキーでアプリケーション"
|
||||||
native: "ブラウザのUI"
|
native: "ブラウザのUI"
|
||||||
|
|
||||||
|
_externalNavigationWarning:
|
||||||
|
title: "外部サイトに移動します"
|
||||||
|
description: "{host}を離れて外部サイトに移動します"
|
||||||
|
trustThisDomain: "このデバイスで今後このドメインを信頼する"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ExternalWebsiteWarn1711008460816 {
|
||||||
|
name = 'ExternalWebsiteWarn1711008460816'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "trustedLinkUrlPatterns" character varying(3072) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "trustedLinkUrlPatterns"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddRejectReports1728177700920 {
|
||||||
|
name = 'AddRejectReports1728177700920'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" ADD "rejectReports" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "rejectReports"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -29,6 +30,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
|
@ -83,6 +85,7 @@ export class ApInboxService {
|
||||||
private apQuestionService: ApQuestionService,
|
private apQuestionService: ApQuestionService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
@ -530,6 +533,12 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> {
|
private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> {
|
||||||
|
// Make sure the source instance is allowed to send reports.
|
||||||
|
const instance = await this.federatedInstanceService.fetch(actor.host);
|
||||||
|
if (instance.rejectReports) {
|
||||||
|
throw new Bull.UnrecoverableError(`Rejecting report from instance: ${actor.host}`);
|
||||||
|
}
|
||||||
|
|
||||||
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
|
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
|
||||||
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
||||||
const uris = getApIds(activity.object);
|
const uris = getApIds(activity.object);
|
||||||
|
|
|
@ -120,6 +120,7 @@ export class MetaEntityService {
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
})),
|
})),
|
||||||
|
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
|
|
|
@ -158,7 +158,12 @@ export class MiInstance {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
public isNSFW: boolean;
|
public isNSFW: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public rejectReports: boolean;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 16384, default: '',
|
length: 16384, default: '',
|
||||||
})
|
})
|
||||||
|
|
|
@ -674,4 +674,12 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public urlPreviewUserAgent: string | null;
|
public urlPreviewUserAgent: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 3072,
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.',
|
||||||
|
})
|
||||||
|
public trustedLinkUrlPatterns: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,6 +273,14 @@ export const packedMetaLiteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
default: 'local',
|
default: 'local',
|
||||||
},
|
},
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -31,15 +31,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const followings = await this.followingsRepository.findBy({
|
const followings = await this.followingsRepository.findBy([
|
||||||
followerHost: ps.host,
|
{
|
||||||
});
|
followeeHost: ps.host,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
followerHost: ps.host,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const pairs = await Promise.all(followings.map(f => Promise.all([
|
const pairs = await Promise.all(followings.map(f => Promise.all([
|
||||||
this.usersRepository.findOneByOrFail({ id: f.followerId }),
|
this.usersRepository.findOneByOrFail({ id: f.followerId }),
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const paramDef = {
|
||||||
host: { type: 'string' },
|
host: { type: 'string' },
|
||||||
isSuspended: { type: 'boolean' },
|
isSuspended: { type: 'boolean' },
|
||||||
isNSFW: { type: 'boolean' },
|
isNSFW: { type: 'boolean' },
|
||||||
|
rejectReports: { type: 'boolean' },
|
||||||
moderationNote: { type: 'string' },
|
moderationNote: { type: 'string' },
|
||||||
},
|
},
|
||||||
required: ['host'],
|
required: ['host'],
|
||||||
|
@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await this.federatedInstanceService.update(instance.id, {
|
await this.federatedInstanceService.update(instance.id, {
|
||||||
suspensionState,
|
suspensionState,
|
||||||
isNSFW: ps.isNSFW,
|
isNSFW: ps.isNSFW,
|
||||||
|
rejectReports: ps.rejectReports,
|
||||||
moderationNote: ps.moderationNote,
|
moderationNote: ps.moderationNote,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,6 +76,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.isNSFW != null && instance.isNSFW !== ps.isNSFW) {
|
||||||
|
const message = ps.rejectReports ? 'setRemoteInstanceNSFW' : 'unsetRemoteInstanceNSFW';
|
||||||
|
this.moderationLogService.log(me, message, {
|
||||||
|
id: instance.id,
|
||||||
|
host: instance.host,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.rejectReports != null && instance.rejectReports !== ps.rejectReports) {
|
||||||
|
const message = ps.rejectReports ? 'rejectRemoteInstanceReports' : 'acceptRemoteInstanceReports';
|
||||||
|
this.moderationLogService.log(me, message, {
|
||||||
|
id: instance.id,
|
||||||
|
host: instance.host,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
|
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
|
||||||
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
|
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
|
|
|
@ -128,7 +128,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
silencedHosts: {
|
silencedHosts: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -526,6 +526,14 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -669,6 +677,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
|
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,6 +176,11 @@ export const paramDef = {
|
||||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -665,6 +670,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.trustedLinkUrlPatterns)) {
|
||||||
|
set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -77,8 +77,12 @@ export const moderationLogTypes = [
|
||||||
'deleteGlobalAnnouncement',
|
'deleteGlobalAnnouncement',
|
||||||
'deleteUserAnnouncement',
|
'deleteUserAnnouncement',
|
||||||
'resetPassword',
|
'resetPassword',
|
||||||
|
'setRemoteInstanceNSFW',
|
||||||
|
'unsetRemoteInstanceNSFW',
|
||||||
'suspendRemoteInstance',
|
'suspendRemoteInstance',
|
||||||
'unsuspendRemoteInstance',
|
'unsuspendRemoteInstance',
|
||||||
|
'rejectRemoteInstanceReports',
|
||||||
|
'acceptRemoteInstanceReports',
|
||||||
'updateRemoteInstanceNote',
|
'updateRemoteInstanceNote',
|
||||||
'markSensitiveDriveFile',
|
'markSensitiveDriveFile',
|
||||||
'unmarkSensitiveDriveFile',
|
'unmarkSensitiveDriveFile',
|
||||||
|
@ -227,6 +231,14 @@ export type ModerationLogPayloads = {
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
userHost: string | null;
|
userHost: string | null;
|
||||||
};
|
};
|
||||||
|
setRemoteInstanceNSFW: {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
unsetRemoteInstanceNSFW: {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
suspendRemoteInstance: {
|
suspendRemoteInstance: {
|
||||||
id: string;
|
id: string;
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -235,6 +247,14 @@ export type ModerationLogPayloads = {
|
||||||
id: string;
|
id: string;
|
||||||
host: string;
|
host: string;
|
||||||
};
|
};
|
||||||
|
rejectRemoteInstanceReports: {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
acceptRemoteInstanceReports: {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
updateRemoteInstanceNote: {
|
updateRemoteInstanceNote: {
|
||||||
id: string;
|
id: string;
|
||||||
host: string;
|
host: string;
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
||||||
:behavior="props.navigationBehavior"
|
:behavior="props.navigationBehavior"
|
||||||
:title="url"
|
:title="url"
|
||||||
@click.prevent="self ? true : promptConfirm()"
|
@click.prevent="self ? true : warningExternalWebsite(url)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -23,7 +23,7 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -49,16 +49,6 @@ if (isEnabledUrlPreview.value) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptConfirm() {
|
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
text: i18n.tsx.confirmRemoteUrl({ x: props.url }),
|
|
||||||
plain: true,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
window.open(props.url, '_blank', 'nofollow noopener popup=false');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
131
packages/frontend/src/components/MkUrlWarningDialog.vue
Normal file
131
packages/frontend/src/components/MkUrlWarningDialog.vue
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||||
|
<div :class="$style.root" class="_gaps">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<div :class="$style.icon">
|
||||||
|
<i class="ti ti-alert-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.title">{{ i18n.ts._externalNavigationWarning.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div><Mfm :text="i18n.tsx._externalNavigationWarning.description({ host: instanceName })"/></div>
|
||||||
|
<div class="_monospace" :class="$style.urlAddress">{{ url }}</div>
|
||||||
|
<div>
|
||||||
|
<MkSwitch v-model="trustThisDomain">{{ i18n.ts._externalNavigationWarning.trustThisDomain }}</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.buttons">
|
||||||
|
<MkButton data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||||
|
<MkButton data-cy-modal-dialog-ok inline primary rounded @click="ok"><i class="ti ti-external-link"></i> {{ i18n.ts.open }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
||||||
|
import { instanceName } from '@/config.js';
|
||||||
|
import MkModal from '@/components/MkModal.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
type Result = string | number | true | null;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
const trustThisDomain = ref(false);
|
||||||
|
|
||||||
|
const domain = computed(() => new URL(props.url).hostname);
|
||||||
|
|
||||||
|
// overload function を使いたいので lint エラーを無視する
|
||||||
|
function done(canceled: true): void;
|
||||||
|
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||||
|
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
|
||||||
|
emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ok() {
|
||||||
|
const result = true;
|
||||||
|
if (!defaultStore.state.trustedDomains.includes(domain.value) && trustThisDomain.value) {
|
||||||
|
await defaultStore.set('trustedDomains', defaultStore.state.trustedDomains.concat(domain.value));
|
||||||
|
}
|
||||||
|
done(false, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
done(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(evt: KeyboardEvent) {
|
||||||
|
if (evt.key === 'Escape') cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
padding: 32px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urlAddress {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -142,6 +142,7 @@ function showMenu(ev: MouseEvent) {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainFg {
|
.mainFg {
|
||||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
||||||
:behavior="props.navigationBehavior"
|
:behavior="props.navigationBehavior"
|
||||||
@contextmenu.stop="() => {}"
|
@contextmenu.stop="() => {}"
|
||||||
|
@click.prevent="self ? true : warningExternalWebsite(props.url)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<template v-if="!self">
|
<template v-if="!self">
|
||||||
|
@ -34,6 +35,7 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||||
|
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkTextarea v-model="trustedLinkUrlPatterns">
|
||||||
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
|
|
||||||
<MkTextarea v-model="sensitiveWords">
|
<MkTextarea v-model="sensitiveWords">
|
||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||||
|
@ -105,6 +111,7 @@ const bubbleTimeline = ref<string>('');
|
||||||
const tosUrl = ref<string | null>(null);
|
const tosUrl = ref<string | null>(null);
|
||||||
const privacyPolicyUrl = ref<string | null>(null);
|
const privacyPolicyUrl = ref<string | null>(null);
|
||||||
const inquiryUrl = ref<string | null>(null);
|
const inquiryUrl = ref<string | null>(null);
|
||||||
|
const trustedLinkUrlPatterns = ref<string>('');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
|
@ -120,6 +127,7 @@ async function init() {
|
||||||
bubbleTimeline.value = meta.bubbleInstances.join('\n');
|
bubbleTimeline.value = meta.bubbleInstances.join('\n');
|
||||||
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
|
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
|
||||||
inquiryUrl.value = meta.inquiryUrl;
|
inquiryUrl.value = meta.inquiryUrl;
|
||||||
|
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -135,6 +143,7 @@ function save() {
|
||||||
hiddenTags: hiddenTags.value.split('\n'),
|
hiddenTags: hiddenTags.value.split('\n'),
|
||||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||||
bubbleInstances: bubbleTimeline.value.split('\n'),
|
bubbleInstances: bubbleTimeline.value.split('\n'),
|
||||||
|
trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
'markSensitiveDriveFile',
|
'markSensitiveDriveFile',
|
||||||
'resetPassword',
|
'resetPassword',
|
||||||
'suspendRemoteInstance',
|
'suspendRemoteInstance',
|
||||||
|
'setRemoteInstanceNSFW',
|
||||||
|
'unsetRemoteInstanceNSFW',
|
||||||
|
'rejectRemoteInstanceReports',
|
||||||
|
'acceptRemoteInstanceReports',
|
||||||
].includes(log.type),
|
].includes(log.type),
|
||||||
[$style.logRed]: [
|
[$style.logRed]: [
|
||||||
'suspend',
|
'suspend',
|
||||||
|
@ -61,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||||
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
|
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||||
|
<span v-else-if="log.type === 'setRemoteInstanceNSFW'">: {{ log.info.host }}</span>
|
||||||
|
<span v-else-if="log.type === 'unsetRemoteInstanceNSFW'">: {{ log.info.host }}</span>
|
||||||
|
<span v-else-if="log.type === 'rejectRemoteInstanceReports'">: {{ log.info.host }}</span>
|
||||||
|
<span v-else-if="log.type === 'acceptRemoteInstanceReports'">: {{ log.info.host }}</span>
|
||||||
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
||||||
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
|
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
|
||||||
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
||||||
|
|
|
@ -43,12 +43,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts._delivery._type[suspensionState] }}
|
{{ i18n.ts._delivery._type[suspensionState] }}
|
||||||
</template>
|
</template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
|
<div class="_buttons">
|
||||||
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
|
<MkButton inline :disabled="!instance" danger @click="deleteAllFiles">{{ i18n.ts.deleteAllFiles }}</MkButton>
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkButton inline :disabled="!instance" danger @click="severAllFollowRelations">{{ i18n.ts.severAllFollowRelations }}</MkButton>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkButton v-if="suspensionState === 'none'" inline :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
|
||||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
<MkButton v-if="suspensionState !== 'none'" inline :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
|
||||||
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
</div>
|
||||||
|
<MkInfo v-if="isBaseBlocked" warn>{{ i18n.ts.blockedByBase }}</MkInfo>
|
||||||
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
|
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
|
||||||
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
|
||||||
|
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
|
||||||
|
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
||||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||||
<MkTextarea v-model="moderationNote" manualSave>
|
<MkTextarea v-model="moderationNote" manualSave>
|
||||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||||
|
@ -156,6 +164,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -170,10 +179,26 @@ const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'au
|
||||||
const isBlocked = ref(false);
|
const isBlocked = ref(false);
|
||||||
const isSilenced = ref(false);
|
const isSilenced = ref(false);
|
||||||
const isNSFW = ref(false);
|
const isNSFW = ref(false);
|
||||||
|
const rejectReports = ref(false);
|
||||||
const isMediaSilenced = ref(false);
|
const isMediaSilenced = ref(false);
|
||||||
const faviconUrl = ref<string | null>(null);
|
const faviconUrl = ref<string | null>(null);
|
||||||
const moderationNote = ref('');
|
const moderationNote = ref('');
|
||||||
|
|
||||||
|
const baseDomains = computed(() => {
|
||||||
|
const domains: string[] = [];
|
||||||
|
|
||||||
|
const parts = props.host.toLowerCase().split('.');
|
||||||
|
for (let s = 1; s < parts.length; s++) {
|
||||||
|
const domain = parts.slice(s).join('.');
|
||||||
|
domains.push(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains;
|
||||||
|
});
|
||||||
|
const isBaseBlocked = computed(() => meta.value && baseDomains.value.some(d => meta.value?.blockedHosts.includes(d)));
|
||||||
|
const isBaseSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.silencedHosts.includes(d)));
|
||||||
|
const isBaseMediaSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.mediaSilencedHosts.includes(d)));
|
||||||
|
|
||||||
const usersPagination = {
|
const usersPagination = {
|
||||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -200,6 +225,7 @@ async function fetch(): Promise<void> {
|
||||||
isBlocked.value = instance.value?.isBlocked ?? false;
|
isBlocked.value = instance.value?.isBlocked ?? false;
|
||||||
isSilenced.value = instance.value?.isSilenced ?? false;
|
isSilenced.value = instance.value?.isSilenced ?? false;
|
||||||
isNSFW.value = instance.value?.isNSFW ?? false;
|
isNSFW.value = instance.value?.isNSFW ?? false;
|
||||||
|
rejectReports.value = instance.value?.rejectReports ?? false;
|
||||||
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
|
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
|
||||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||||
moderationNote.value = instance.value?.moderationNote ?? '';
|
moderationNote.value = instance.value?.moderationNote ?? '';
|
||||||
|
@ -260,6 +286,14 @@ async function toggleNSFW(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRejectReports(): Promise<void> {
|
||||||
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
await misskeyApi('admin/federation/update-instance', {
|
||||||
|
host: instance.value.host,
|
||||||
|
rejectReports: rejectReports.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function refreshMetadata(): void {
|
function refreshMetadata(): void {
|
||||||
if (!instance.value) throw new Error('No instance?');
|
if (!instance.value) throw new Error('No instance?');
|
||||||
misskeyApi('admin/federation/refresh-remote-instance-metadata', {
|
misskeyApi('admin/federation/refresh-remote-instance-metadata', {
|
||||||
|
@ -270,6 +304,43 @@ function refreshMetadata(): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteAllFiles(): void {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'danger',
|
||||||
|
text: i18n.ts.deleteAllFilesConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
|
||||||
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
await misskeyApi('admin/federation/delete-all-files', {
|
||||||
|
host: instance.value.host,
|
||||||
|
});
|
||||||
|
await os.alert({
|
||||||
|
text: i18n.ts.deleteAllFilesQueued,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function severAllFollowRelations(): void {
|
||||||
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'danger',
|
||||||
|
text: i18n.tsx.severAllFollowRelationsConfirm({
|
||||||
|
instanceName: meta.value.shortName ?? meta.value.name,
|
||||||
|
followingCount: instance.value.followingCount,
|
||||||
|
followersCount: instance.value.followersCount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
|
||||||
|
await misskeyApi('admin/federation/remove-all-following', {
|
||||||
|
host: instance.value.host,
|
||||||
|
});
|
||||||
|
await os.alert({
|
||||||
|
text: i18n.tsx.severAllFollowRelationsQueued({ host: instance.value.host }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fetch();
|
fetch();
|
||||||
|
|
||||||
const headerActions = computed(() => [{
|
const headerActions = computed(() => [{
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
|
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
|
||||||
|
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||||
|
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const pluginContexts = new Map<string, Interpreter>();
|
const pluginContexts = new Map<string, Interpreter>();
|
||||||
|
@ -92,16 +93,8 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
|
||||||
registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
|
registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
|
||||||
}),
|
}),
|
||||||
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
||||||
(async () => {
|
utils.assertString(url);
|
||||||
utils.assertString(url);
|
warningExternalWebsite(url.value);
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
text: i18n.tsx.confirmRemoteUrl({ x: url.value }),
|
|
||||||
plain: true,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
window.open(url.value, '_blank', 'noopener');
|
|
||||||
})();
|
|
||||||
}),
|
}),
|
||||||
'Plugin:config': values.OBJ(config),
|
'Plugin:config': values.OBJ(config),
|
||||||
};
|
};
|
||||||
|
|
51
packages/frontend/src/scripts/warning-external-website.ts
Normal file
51
packages/frontend/src/scripts/warning-external-website.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue';
|
||||||
|
|
||||||
|
const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i;
|
||||||
|
const isRegExp = /^\/(.+)\/(.*)$/;
|
||||||
|
|
||||||
|
export async function warningExternalWebsite(url: string) {
|
||||||
|
const domain = extractDomain.exec(url)?.[4];
|
||||||
|
|
||||||
|
if (!domain) return false;
|
||||||
|
|
||||||
|
const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => {
|
||||||
|
const r = isRegExp.exec(expression);
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
return new RegExp(r[1], r[2]).test(url);
|
||||||
|
} else if (expression.includes(' ')) {
|
||||||
|
return expression.split(' ').every(keyword => url.includes(keyword));
|
||||||
|
} else {
|
||||||
|
return domain.endsWith(expression);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain);
|
||||||
|
|
||||||
|
if (!isTrustedByInstance && !isTrustedByUser) {
|
||||||
|
const confirm = await new Promise<{ canceled: boolean }>(resolve => {
|
||||||
|
const { dispose } = os.popup(MkUrlWarningDialog, {
|
||||||
|
url,
|
||||||
|
}, {
|
||||||
|
done: result => {
|
||||||
|
resolve(result ?? { canceled: true });
|
||||||
|
},
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirm.canceled) return false;
|
||||||
|
|
||||||
|
return window.open(url, '_blank', 'nofollow noopener popup=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.open(url, '_blank', 'nofollow noopener popup=false');
|
||||||
|
}
|
|
@ -165,6 +165,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
default: 'public' as 'public' | 'home' | 'followers',
|
default: 'public' as 'public' | 'home' | 'followers',
|
||||||
},
|
},
|
||||||
|
trustedDomains: {
|
||||||
|
where: 'account',
|
||||||
|
default: [] as string[],
|
||||||
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
where: 'deviceAccount',
|
where: 'deviceAccount',
|
||||||
|
|
|
@ -5114,6 +5114,7 @@ export type components = {
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
noteSearchableScope: 'local' | 'global';
|
noteSearchableScope: 'local' | 'global';
|
||||||
|
trustedLinkUrlPatterns: string[];
|
||||||
};
|
};
|
||||||
MetaDetailedOnly: {
|
MetaDetailedOnly: {
|
||||||
features?: {
|
features?: {
|
||||||
|
@ -5215,7 +5216,7 @@ export type operations = {
|
||||||
enableEmail: boolean;
|
enableEmail: boolean;
|
||||||
enableServiceWorker: boolean;
|
enableServiceWorker: boolean;
|
||||||
translatorAvailable: boolean;
|
translatorAvailable: boolean;
|
||||||
silencedHosts?: string[];
|
silencedHosts: string[];
|
||||||
mediaSilencedHosts: string[];
|
mediaSilencedHosts: string[];
|
||||||
pinnedUsers: string[];
|
pinnedUsers: string[];
|
||||||
hiddenTags: string[];
|
hiddenTags: string[];
|
||||||
|
@ -5310,6 +5311,7 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength: boolean;
|
urlPreviewRequireContentLength: boolean;
|
||||||
urlPreviewUserAgent: string | null;
|
urlPreviewUserAgent: string | null;
|
||||||
urlPreviewSummaryProxyUrl: string | null;
|
urlPreviewSummaryProxyUrl: string | null;
|
||||||
|
trustedLinkUrlPatterns: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9831,6 +9833,7 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength?: boolean;
|
urlPreviewRequireContentLength?: boolean;
|
||||||
urlPreviewUserAgent?: string | null;
|
urlPreviewUserAgent?: string | null;
|
||||||
urlPreviewSummaryProxyUrl?: string | null;
|
urlPreviewSummaryProxyUrl?: string | null;
|
||||||
|
trustedLinkUrlPatterns?: string[] | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue