Merge branch 'misskey-dev:develop' into repair-style
This commit is contained in:
commit
eebdb35dda
65 changed files with 1832 additions and 285 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -9,18 +9,36 @@
|
|||
You should also include the user name that made the change.
|
||||
-->
|
||||
|
||||
## 12.x.x (unreleased)
|
||||
## 12.112.2 (2022/07/08)
|
||||
|
||||
### Bugfixes
|
||||
- Fix Docker doesn't work @mei23
|
||||
Still not working on arm64 environment. (See 12.112.0)
|
||||
|
||||
## 12.112.1 (2022/07/07)
|
||||
same as 12.112.0
|
||||
|
||||
## 12.112.0 (2022/07/07)
|
||||
|
||||
### Known issues
|
||||
- 現在arm64環境ではインストールに失敗します。これは次のバージョンで修正される予定です。
|
||||
|
||||
### Changes
|
||||
- ハイライトがみつけるに統合されました
|
||||
- カスタム絵文字ページはインスタンス情報ページに統合されました
|
||||
- 連合ページはインスタンス情報ページに統合されました
|
||||
- メンション一覧ページは通知一覧ページに統合されました
|
||||
- ダイレクト投稿一覧ページは通知一覧ページに統合されました
|
||||
- メニューからアンテナタイムラインを表示する方法は廃止され、タイムライン上部のアイコンからアクセスするようになりました
|
||||
- メニューからリストタイムラインを表示する方法は廃止され、タイムライン上部のアイコンからアクセスするようになりました
|
||||
|
||||
### Improvements
|
||||
- Server: Allow GET method for some endpoints @syuilo
|
||||
- Server: Auto NSFW detection @syuilo
|
||||
- Server: Add rate limit to i/notifications @tamaina
|
||||
- Client: Improve control panel @syuilo
|
||||
- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
|
||||
- Client: Statusbars @syuilo
|
||||
- Client: Add instance-cloud widget @syuilo
|
||||
- Client: Add rss-ticker widget @syuilo
|
||||
- Client: Removing entries from a clip @futchitwo
|
||||
|
@ -29,6 +47,8 @@ You should also include the user name that made the change.
|
|||
- Client: Word mute also checks content warnings @Johann150
|
||||
- Client: メニューからページをリロードできるように @syuilo
|
||||
- Client: Improve emoji picker performance @syuilo
|
||||
- Client: For notes with specified visibility, show recipients when hovering over visibility symbol. @Johann150
|
||||
- Client: Make widgets available again on a tablet @syuilo
|
||||
- ユーザーにモデレーションメモを残せる機能 @syuilo
|
||||
- Make possible to delete an account by admin @syuilo
|
||||
- Improve player detection in URL preview @mei23
|
||||
|
|
6
COPYING
6
COPYING
|
@ -1,5 +1,5 @@
|
|||
Unless otherwise stated this repository is
|
||||
Copyright © 2014-2020 syuilo and contributers
|
||||
Copyright © 2014-2022 syuilo and contributers
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
|
@ -13,3 +13,7 @@ https://github.com/muan/emojilib/blob/master/LICENSE
|
|||
RsaSignature2017 implementation by Transmute Industries Inc
|
||||
License: MIT
|
||||
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
|
||||
|
||||
Machine learning model for sensitive images by Infinite Red, Inc.
|
||||
License: MIT
|
||||
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
|
||||
|
|
28
Dockerfile
28
Dockerfile
|
@ -1,28 +1,24 @@
|
|||
FROM node:18.0.0-alpine3.15 AS base
|
||||
FROM node:16.15.1-bullseye AS builder
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
WORKDIR /misskey
|
||||
|
||||
ENV BUILD_DEPS autoconf automake file g++ gcc libc-dev libtool make nasm pkgconfig python3 zlib-dev git
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN apk add --no-cache $BUILD_DEPS && \
|
||||
git submodule update --init && \
|
||||
yarn install && \
|
||||
yarn build && \
|
||||
rm -rf .git
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential
|
||||
RUN git submodule update --init
|
||||
RUN yarn install
|
||||
RUN yarn build
|
||||
RUN rm -rf .git
|
||||
|
||||
FROM base AS runner
|
||||
FROM node:16.15.1-bullseye-slim AS runner
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ffmpeg \
|
||||
tini
|
||||
WORKDIR /misskey
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y ffmpeg tini
|
||||
|
||||
COPY --from=builder /misskey/node_modules ./node_modules
|
||||
COPY --from=builder /misskey/built ./built
|
||||
|
@ -32,5 +28,5 @@ COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node
|
|||
COPY . ./
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["npm", "run", "migrateandstart"]
|
||||
|
||||
|
|
|
@ -869,6 +869,32 @@ logoutConfirm: "ログアウトしますか?"
|
|||
lastActiveDate: "最終利用日時"
|
||||
statusbar: "ステータスバー"
|
||||
pleaseSelect: "選択してください"
|
||||
reverse: "反転"
|
||||
colored: "色付き"
|
||||
refreshInterval: "更新間隔"
|
||||
label: "ラベル"
|
||||
type: "タイプ"
|
||||
speed: "速度"
|
||||
slow: "遅い"
|
||||
fast: "速い"
|
||||
sensitiveMediaDetection: "センシティブなメディアの検出"
|
||||
localOnly: "ローカルのみ"
|
||||
remoteOnly: "リモートのみ"
|
||||
failedToUpload: "アップロード失敗"
|
||||
cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。"
|
||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||
beta: "ベータ"
|
||||
enableAutoSensitive: "自動NSFW判定"
|
||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
sensitivity: "検出感度"
|
||||
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
|
||||
setSensitiveFlagAutomatically: "NSFWフラグを設定する"
|
||||
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
|
||||
analyzeVideos: "動画の解析を有効化"
|
||||
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
|
||||
|
||||
_emailUnavailable:
|
||||
used: "既に使用されています"
|
||||
|
|
|
@ -203,6 +203,7 @@ done: "เสร็จสิ้น"
|
|||
processing: "กำลังประมวลผล..."
|
||||
preview: "แสดงตัวอย่าง"
|
||||
default: "ค่าตั้งต้น"
|
||||
defaultValueIs: "ค่าเริ่มต้น: {value}"
|
||||
noCustomEmojis: "ไม่มีอีโมจิ"
|
||||
noJobs: "ไม่มีชิ้นงาน"
|
||||
federating: "สหพันธ์"
|
||||
|
@ -381,6 +382,7 @@ administrator: "ผู้ดูแลระบบ"
|
|||
token: "โทเค็น"
|
||||
twoStepAuthentication: "ยืนยันตัวตน 2 ชั้น"
|
||||
moderator: "ผู้ควบคุม"
|
||||
moderation: "การกลั่นกรอง"
|
||||
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
|
||||
securityKey: "กุญแจความปลอดภัย"
|
||||
securityKeyName: "ชื่อคีย์"
|
||||
|
@ -485,6 +487,26 @@ objectStorageBaseUrlDesc: "URL ที่ใช้เป็นข้อมูล
|
|||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "โปรดระบุชื่อที่เก็บข้อมูลที่ใช้กับผู้ให้บริการของคุณ"
|
||||
objectStoragePrefix: "คำนำหน้า"
|
||||
objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้นะ"
|
||||
objectStorageEndpoint: "ปลายทาง"
|
||||
objectStorageEndpointDesc: "เว้นว่างไว้หากคุณใช้ AWS S3 หรือระบุปลายทางเป็น '<host>' หรือ '<host>:<port>' ทั้งนี้ขึ้นอยู่กับผู้ให้บริการที่คุณใช้อยู่ด้วย"
|
||||
objectStorageRegion: "ภูมิภาค"
|
||||
objectStorageRegionDesc: "ระบุภูมิภาค เช่น 'xx-east-1' ถ้าหากบริการของคุณไม่ได้แยกความแตกต่างระหว่างภูมิภาคก็ให้ เว้นว่างไว้หรือป้อน 'us-east-1'"
|
||||
objectStorageUseSSL: "ใช้ SSL"
|
||||
objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้ ถ้าหากคุณจะไม่ใช้ HTTPS สำหรับการเชื่อมต่อ API"
|
||||
objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี"
|
||||
objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API"
|
||||
objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด"
|
||||
serverLogs: "บันทึกของเซิร์ฟเวอร์"
|
||||
deleteAll: "ลบทั้งหมด"
|
||||
showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
|
||||
newNoteRecived: "มีโน้ตใหม่"
|
||||
sounds: "เสียง"
|
||||
listen: "ฟัง"
|
||||
none: "ไม่มี"
|
||||
showInPage: "แสดงในเพจ"
|
||||
popout: "ป๊อปเอาต์"
|
||||
volume: "ความดัง"
|
||||
masterVolume: "มาสเตอร์วอลุ่ม"
|
||||
details: "รายละเอียด"
|
||||
chooseEmoji: "เลือกโมจิของเธอ"
|
||||
|
@ -512,6 +534,7 @@ removeAllFollowing: "เลิกติดตามผู้ใช้ที่
|
|||
removeAllFollowingDescription: "การที่คุณดำเนินการนี้จะเลิกติดตามบัญชีทั้งหมดจาก {host} โปรดเรียกใช้คำสั่งสิ่งนี้หากต้องการยกเลิกอินสแตนซ์ เช่น ไม่มีอยู่แล้ว"
|
||||
userSuspended: "ผู้ใช้รายนี้ถูกระงับการใช้งาน"
|
||||
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
|
||||
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
||||
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
||||
menu: "เมนู"
|
||||
divider: "ตัวแบ่ง"
|
||||
|
@ -534,7 +557,36 @@ themeEditor: "ตัวแก้ไขธีม"
|
|||
description: "รายละเอียด"
|
||||
describeFile: "เพิ่มแคปชั่น"
|
||||
enterFileDescription: "ใส่แคปชั่น"
|
||||
author: "ผู้เขียน"
|
||||
leaveConfirm: "คุณมีการเปลี่ยนแปลงที่ไม่ได้บันทึกนะ นายต้องการทิ้งการเปลี่ยนแปลงเหล่านั้นหรอ?"
|
||||
manage: "การจัดการ"
|
||||
plugins: "ปลั๊กอิน"
|
||||
deck: "เด็ค"
|
||||
undeck: "ออกจากเด็ค"
|
||||
useBlurEffectForModal: "ใช้เอฟเฟกต์เบลอสำหรับโมดอล"
|
||||
useFullReactionPicker: "ใช้เครื่องมือเลือกปฏิกิริยาขนาดเต็ม"
|
||||
width: "ความกว้าง"
|
||||
height: "ความสูง"
|
||||
large: "ใหญ่"
|
||||
medium: "ปานกลาง"
|
||||
small: "เล็ก"
|
||||
generateAccessToken: "สร้างการเข้าถึงโทเค็น"
|
||||
permission: "การอนุญาต"
|
||||
enableAll: "เปิดใช้งานทั้งหมด"
|
||||
disableAll: "ปิดการใช้งานทั้งหมด"
|
||||
tokenRequested: "ให้สิทธิ์การเข้าถึงบัญชี"
|
||||
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
||||
notificationType: "ประเภทการแจ้งเตือน"
|
||||
edit: "แก้ไข"
|
||||
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
|
||||
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
||||
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
||||
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
||||
email: "อีเมล์"
|
||||
emailAddress: "ที่อยู่อีเมล์"
|
||||
smtpConfig: "กำหนดค่าเซิร์ฟเวอร์ SMTP"
|
||||
smtpHost: "โฮสต์"
|
||||
smtpPort: "พอร์ต"
|
||||
smtpUser: "ชื่อผู้ใช้"
|
||||
smtpPass: "รหัสผ่าน"
|
||||
emptyToDisableSmtpAuth: "ปล่อยชื่อผู้ใช้และรหัสผ่านว่างไว้เพื่อปิดใช้งานการยืนยัน SMTP"
|
||||
|
@ -589,12 +641,166 @@ random: "สุ่มค่า"
|
|||
system: "ระบบ"
|
||||
switchUi: "สลับ UI"
|
||||
desktop: "เดสก์ท็อป"
|
||||
clip: "คลิป"
|
||||
createNew: "สร้างใหม่"
|
||||
optional: "ไม่บังคับ"
|
||||
createNewClip: "สร้างคลิปใหม่"
|
||||
unclip: "ลบคลิป"
|
||||
confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป \"{name}\" แล้ว คุณต้องการลบออกจากคลิปนี้แทนอย่างงั้นหรอ?"
|
||||
public: "สาธารณะ"
|
||||
i18nInfo: "Misskey กำลังได้รับการแปลเป็นภาษาต่างๆ โดยอาสาสมัคร คุณสามารถช่วยเหลือได้ที่ {link}"
|
||||
manageAccessTokens: "การจัดการโทเค็นการเข้าถึง"
|
||||
accountInfo: "ข้อมูลบัญชี"
|
||||
notesCount: "จำนวนของโน้ต"
|
||||
repliesCount: "จำนวนการตอบกลับที่ส่ง"
|
||||
renotesCount: "จำนวนรีโน้ตที่ส่ง"
|
||||
repliedCount: "จำนวนของการตอบกลับที่ได้รับ"
|
||||
renotedCount: "จำนวนรีโน้ตที่ได้รับ"
|
||||
followingCount: "จำนวนบัญชีที่ติดตาม"
|
||||
followersCount: "จำนวนผู้ติดตาม"
|
||||
sentReactionsCount: "จำนวนปฏิกิริยาที่ส่ง"
|
||||
receivedReactionsCount: "จำนวนปฏิกิริยาที่ได้รับ"
|
||||
pollVotesCount: "จำนวนโหวตที่ส่งไป"
|
||||
pollVotedCount: "จำนวนโหวตที่ได้รับ"
|
||||
yes: "ใช่"
|
||||
no: "ไม่"
|
||||
driveFilesCount: "จำนวนไฟล์ไดรฟ์"
|
||||
driveUsage: "การใช้พื้นที่ไดรฟ์"
|
||||
noCrawle: "ปฏิเสธการจัดทำดัชนีของโปรแกรมรวบรวมข้อมูล"
|
||||
noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ บันทึกย่อ หน้า ฯลฯ"
|
||||
lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องตั้งค่าการเปิดเผยโน้ตเป็น \"ผู้ติดตามเท่านั้น\" โน้ตย่อของคุณจะปรากฏแก่ทุกคน ถึงแม้ว่าคุณจะเป็นกำหนดให้ผู้ติดตามต้องได้รับการอนุมัติด้วยตนเองก็ตาม"
|
||||
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
|
||||
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
||||
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
||||
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
|
||||
notSet: "ไม่ได้ตั้งค่า"
|
||||
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
|
||||
noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ"
|
||||
pageLikesCount: "จำนวนเพจที่ชอบ"
|
||||
pageLikedCount: "จำนวนการกดถูกใจเพจที่ได้รับแล้ว"
|
||||
contact: "ติดต่อ"
|
||||
useSystemFont: "ใช้ฟอนต์เริ่มต้นของระบบ"
|
||||
clips: "คลิป"
|
||||
experimentalFeatures: "ฟังก์ชั่นทดสอบ"
|
||||
developer: "สำหรับนักพัฒนา"
|
||||
makeExplorable: "ทำให้บัญชีมองเห็นใน \"สำรวจ\""
|
||||
makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน \"สำรวจ\" นะ"
|
||||
showGapBetweenNotesInTimeline: "แสดงช่องว่างระหว่างโพสต์บนไทม์ไลน์"
|
||||
duplicate: "ทำซ้ำ"
|
||||
left: "ซ้าย"
|
||||
center: "ศูนย์กลาง"
|
||||
wide: "กว้าง"
|
||||
narrow: "ชิด"
|
||||
reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยมั้ย"
|
||||
needReloadToApply: "จำเป็นต้องโหลดซ้ำถึงจะมีผลนะ"
|
||||
showTitlebar: "แสดงแถบชื่อ"
|
||||
clearCache: "ล้างแคช"
|
||||
onlineUsersCount: "{n} ผู้ใช้คนนี้กำลังออนไลน์"
|
||||
nUsers: "{n} ผู้ใช้งาน"
|
||||
nNotes: "{n} โน้ต"
|
||||
sendErrorReports: "ส่งรายงานว่าข้อผิดพลาด"
|
||||
sendErrorReportsDescription: "เมื่อเปิดใช้งาน ข้อมูลข้อผิดพลาดโดยรายละเอียดนั้นจะถูกแชร์ให้กับ Misskey เมื่อเกิดปัญหา ซึ่งช่วยปรับปรุงคุณภาพของ Misskey\nซึ่งจะรวมถึงข้อมูล เช่น เวอร์ชั่นของระบบปฏิบัติการ เบราว์เซอร์ที่คุณใช้ กิจกรรมของคุณใน Misskey เป็นต้น"
|
||||
myTheme: "ธีมของฉัน"
|
||||
backgroundColor: "ภาพพื้นหลัง"
|
||||
accentColor: "รูปแบบสี"
|
||||
textColor: "สีข้อความ"
|
||||
saveAs: "บันทึกเป็น..."
|
||||
advanced: "ขั้นสูง"
|
||||
value: "ค่า"
|
||||
createdAt: "สร้างเมื่อ"
|
||||
updatedAt: "อัพเดทล่าสุด"
|
||||
saveConfirm: "บันทึกเปลี่ยนแปลงมั้ย?"
|
||||
deleteConfirm: "ลบจริงๆเหรอ?"
|
||||
invalidValue: "ค่านี้ไม่ถูกต้อง"
|
||||
registry: "ทะเบียน"
|
||||
closeAccount: "ปิด บัญชี"
|
||||
currentVersion: "เวอร์ชั่นปัจจุบัน"
|
||||
latestVersion: "รุ่นปัจจุบัน"
|
||||
youAreRunningUpToDateClient: "คุณกำลังใช้ไคลเอ็นต์เวอร์ชันใหม่ล่าสุดนะ"
|
||||
newVersionOfClientAvailable: "มีไคลเอ็นต์เวอร์ชันใหม่กว่าของคุณพร้อมใช้งานนะ"
|
||||
usageAmount: "การใช้งาน"
|
||||
capacity: "ความจุ"
|
||||
inUse: "ใช้แล้ว"
|
||||
editCode: "แก้ไขโค้ด"
|
||||
apply: "ตกลง"
|
||||
receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากอินสแตนซ์นี้"
|
||||
emailNotification: "การแจ้งเตือนทางอีเมล์"
|
||||
publish: "เผยแพร่"
|
||||
inChannelSearch: "ค้นหาในช่อง"
|
||||
useReactionPickerForContextMenu: "เปิดตัวเลือกปฏิกิริยาเมื่อคลิกขวา"
|
||||
typingUsers: "{users} กำลัง/กำลังพิมพ์..."
|
||||
jumpToSpecifiedDate: "ข้ามไปยังวันที่เฉพาะเจาะจง"
|
||||
showingPastTimeline: "กำลังแสดงผลไทม์ไลน์เก่า"
|
||||
clear: "ล้าง"
|
||||
markAllAsRead: "ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว"
|
||||
goBack: "ย้อนกลับ"
|
||||
unlikeConfirm: "ลบไลค์ของคุณออกจริงๆหรอ"
|
||||
fullView: "มุมมองแบบเต็ม"
|
||||
quitFullView: "ออกจากมุมมองแบบเต็ม"
|
||||
addDescription: "เพิ่มคำอธิบาย"
|
||||
userPagePinTip: "คุณสามารถแสดงผลโน้ตย่อได้ที่นี่โดยเลือก \"ปักหมุดที่โปรไฟล์\" จากเมนูของโน้ตย่อแต่ละรายการนะ"
|
||||
notSpecifiedMentionWarning: "โน้ตนี้มีการกล่าวถึงผู้ใช้งานที่ไม่รวมอยู่ในผู้รับ"
|
||||
info: "เกี่ยวกับ"
|
||||
userInfo: "ข้อมูลผู้ใช้"
|
||||
unknown: "ไม่ทราบสถานะ"
|
||||
onlineStatus: "สถานะออนไลน์"
|
||||
hideOnlineStatus: "ซ่อนสถานะออนไลน์"
|
||||
hideOnlineStatusDescription: "การซ่อนสถานะออนไลน์ของคุณช่วยลดความสะดวกของคุณสมบัติบางอย่าง เช่น การค้นหา อ่ะนะ"
|
||||
online: "ออนไลน์"
|
||||
active: "ใช้งานอยู่"
|
||||
offline: "ออฟไลน์"
|
||||
notRecommended: "ไม่ใช้งาน"
|
||||
botProtection: "การป้องกัน Bot (or AI)"
|
||||
instanceBlocking: "อินสแตนซ์ที่ถูกบล็อก"
|
||||
selectAccount: "เลือกบัญชี"
|
||||
switchAccount: "สลับบัญชีผู้ใช้"
|
||||
enabled: "เปิดใช้งาน"
|
||||
disabled: "ปิดการใช้งาน"
|
||||
quickAction: "ปุ่มลัด"
|
||||
user: "ผู้ใช้งาน"
|
||||
administration: "การจัดการ"
|
||||
accounts: "บัญชีผู้ใช้"
|
||||
switch: "สลับ"
|
||||
noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม่ได้รับการกำหนดค่านะ"
|
||||
noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ"
|
||||
configure: "กำหนดค่า"
|
||||
postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่"
|
||||
gallery: "แกลเลอรี่"
|
||||
recentPosts: "โพสต์ล่าสุด"
|
||||
popularPosts: "โพสต์ติดอันดับ"
|
||||
shareWithNote: "แบ่งปันด้วยโน้ต"
|
||||
ads: "โฆษณา"
|
||||
expiration: "กำหนดเวลา"
|
||||
memo: "ข้อควรจำ"
|
||||
priority: "ลำดับความสำคัญ"
|
||||
high: "สูง"
|
||||
middle: "ปานกลาง"
|
||||
low: "ต่ำ"
|
||||
emailNotConfiguredWarning: "ไม่ได้ตั้งค่าที่อยู่อีเมลนะ"
|
||||
ratio: "อัตราส่วน"
|
||||
previewNoteText: "แสดงตัวอย่าง"
|
||||
customCss: "CSS ที่กำหนดเอง"
|
||||
customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้ว่าการตั้งค่านี้ใช้ทำอะไร การป้อนค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์หยุดทำงานตามปกติได้นะ"
|
||||
global: "ทั่วโลก"
|
||||
squareAvatars: "แสดงผลอวตารสี่เหลี่ยม"
|
||||
sent: "ส่ง"
|
||||
received: "ได้รับแล้ว"
|
||||
searchResult: "ผลการค้นหา"
|
||||
hashtags: "แฮชแท็ก"
|
||||
troubleshooting: "แก้ปัญหา"
|
||||
useBlurEffect: "ใช้เอฟเฟกต์เบลอใน UI"
|
||||
learnMore: "แสดงให้ดูหน่อย"
|
||||
misskeyUpdated: "Misskey ได้รับการอัปเดตแล้ว!"
|
||||
whatIsNew: "แสดงการเปลี่ยนแปลง"
|
||||
translate: "แปลภาษา"
|
||||
translatedFrom: "แปลมาจาก {x}"
|
||||
accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่"
|
||||
searchByGoogle: "ค้นหา"
|
||||
file: "ไฟล์"
|
||||
_ffVisibility:
|
||||
public: "เผยแพร่"
|
||||
_ad:
|
||||
back: "ย้อนกลับ"
|
||||
_email:
|
||||
_follow:
|
||||
title: "ได้ติดตามคุณ"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "12.112.0-beta.20",
|
||||
"version": "12.112.2",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
23
packages/backend/migration/1655368940105-nsfw-detection.js
Normal file
23
packages/backend/migration/1655368940105-nsfw-detection.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
export class nsfwDetection1655368940105 {
|
||||
name = 'nsfwDetection1655368940105'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "forceIsSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "predictedIsSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."predictedIsSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetection" "public"."meta_sensitiveimagedetection_enum" NOT NULL DEFAULT 'none'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "forceIsSensitiveWhenPredicted" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fc2d74a6d7d8b11292a851d8f8" ON "drive_file" ("predictedIsSensitive") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_fc2d74a6d7d8b11292a851d8f8"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forceIsSensitiveWhenPredicted"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetection"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetection_enum"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."predictedIsSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "predictedIsSensitive"`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "forceIsSensitive"`);
|
||||
}
|
||||
}
|
15
packages/backend/migration/1655371960534-nsfw-detection-2.js
Normal file
15
packages/backend/migration/1655371960534-nsfw-detection-2.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export class nsfwDetection21655371960534 {
|
||||
name = 'nsfwDetection21655371960534'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetectionSensitivity" "public"."meta_sensitiveimagedetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "disallowUploadWhenPredictedAsPorn" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disallowUploadWhenPredictedAsPorn"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetectionSensitivity"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
|
||||
}
|
||||
}
|
21
packages/backend/migration/1655388169582-nsfw-detection-3.js
Normal file
21
packages/backend/migration/1655388169582-nsfw-detection-3.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class nsfwDetection31655388169582 {
|
||||
name = 'nsfwDetection31655388169582'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" RENAME TO "meta_sensitiveimagedetectionsensitivity_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" USING "sensitiveImageDetectionSensitivity"::"text"::"public"."meta_sensitiveimagedetectionsensitivity_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" SET DEFAULT 'medium'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" AS ENUM('medium', 'low', 'high')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" USING "sensitiveImageDetectionSensitivity"::"text"::"public"."meta_sensitiveimagedetectionsensitivity_enum_old"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" SET DEFAULT 'medium'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" RENAME TO "meta_sensitiveimagedetectionsensitivity_enum"`);
|
||||
}
|
||||
}
|
25
packages/backend/migration/1655393015659-nsfw-detection-4.js
Normal file
25
packages/backend/migration/1655393015659-nsfw-detection-4.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
export class nsfwDetection41655393015659 {
|
||||
name = 'nsfwDetection41655393015659'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetection"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetection_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetectionSensitivity"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetection" "public"."meta_sensitivemediadetection_enum" NOT NULL DEFAULT 'none'`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionSensitivity" "public"."meta_sensitivemediadetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionSensitivity"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetectionsensitivity_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetection"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetection_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetectionSensitivity" "public"."meta_sensitiveimagedetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetection" "public"."meta_sensitiveimagedetection_enum" NOT NULL DEFAULT 'none'`);
|
||||
}
|
||||
}
|
33
packages/backend/migration/1656251734807-nsfw-detection-5.js
Normal file
33
packages/backend/migration/1656251734807-nsfw-detection-5.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
export class nsfwDetection51656251734807 {
|
||||
name = 'nsfwDetection51656251734807'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_fc2d74a6d7d8b11292a851d8f8"`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "forceIsSensitive"`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "predictedIsSensitive"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forceIsSensitiveWhenPredicted"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disallowUploadWhenPredictedAsPorn"`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybeSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."maybeSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybePorn" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "setSensitiveFlagAutomatically" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3b33dff77bb64b23c88151d23e" ON "drive_file" ("maybeSensitive") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8bdcd3dd2bddb78014999a16ce" ON "drive_file" ("maybePorn") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8bdcd3dd2bddb78014999a16ce"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3b33dff77bb64b23c88151d23e"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoSensitive"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "setSensitiveFlagAutomatically"`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybePorn"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."maybeSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybeSensitive"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "disallowUploadWhenPredictedAsPorn" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "forceIsSensitiveWhenPredicted" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "predictedIsSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ADD "forceIsSensitive" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fc2d74a6d7d8b11292a851d8f8" ON "drive_file" ("predictedIsSensitive") `);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1656408772602-nsfw-detection-6.js
Normal file
11
packages/backend/migration/1656408772602-nsfw-detection-6.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export class nsfwDetection61656408772602 {
|
||||
name = 'nsfwDetection61656408772602'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSensitiveMediaDetectionForVideos" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSensitiveMediaDetectionForVideos"`);
|
||||
}
|
||||
}
|
BIN
packages/backend/nsfw-model/group1-shard1of6
Normal file
BIN
packages/backend/nsfw-model/group1-shard1of6
Normal file
Binary file not shown.
2
packages/backend/nsfw-model/group1-shard2of6
Normal file
2
packages/backend/nsfw-model/group1-shard2of6
Normal file
File diff suppressed because one or more lines are too long
3
packages/backend/nsfw-model/group1-shard3of6
Normal file
3
packages/backend/nsfw-model/group1-shard3of6
Normal file
File diff suppressed because one or more lines are too long
3
packages/backend/nsfw-model/group1-shard4of6
Normal file
3
packages/backend/nsfw-model/group1-shard4of6
Normal file
File diff suppressed because one or more lines are too long
18
packages/backend/nsfw-model/group1-shard5of6
Normal file
18
packages/backend/nsfw-model/group1-shard5of6
Normal file
File diff suppressed because one or more lines are too long
3
packages/backend/nsfw-model/group1-shard6of6
Normal file
3
packages/backend/nsfw-model/group1-shard6of6
Normal file
File diff suppressed because one or more lines are too long
1
packages/backend/nsfw-model/model.json
Normal file
1
packages/backend/nsfw-model/model.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -23,6 +23,7 @@
|
|||
"@peertube/http-signature": "1.6.0",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@tensorflow/tfjs-node": "3.18.0",
|
||||
"abort-controller": "3.0.0",
|
||||
"ajv": "8.11.0",
|
||||
"archiver": "5.3.1",
|
||||
|
@ -36,6 +37,7 @@
|
|||
"cbor": "8.1.0",
|
||||
"chalk": "5.0.1",
|
||||
"chalk-template": "0.4.0",
|
||||
"chokidar": "3.3.1",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
|
@ -74,6 +76,7 @@
|
|||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.2.6",
|
||||
"nodemailer": "6.7.6",
|
||||
"nsfwjs": "2.4.1",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "7.0.0",
|
||||
"pg": "8.7.3",
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import * as stream from 'node:stream';
|
||||
import * as util from 'node:util';
|
||||
import { FSWatcher } from 'chokidar';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import FFmpeg from 'fluent-ffmpeg';
|
||||
import isSvg from 'is-svg';
|
||||
import probeImageSize from 'probe-image-size';
|
||||
import { type predictionType } from 'nsfwjs';
|
||||
import sharp from 'sharp';
|
||||
import { encode } from 'blurhash';
|
||||
import { detectSensitive } from '@/services/detect-sensitive.js';
|
||||
import { createTempDir } from './create-temp.js';
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
|
@ -21,6 +27,8 @@ export type FileInfo = {
|
|||
height?: number;
|
||||
orientation?: number;
|
||||
blurhash?: string;
|
||||
sensitive: boolean;
|
||||
porn: boolean;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
|
@ -37,7 +45,12 @@ const TYPE_SVG = {
|
|||
/**
|
||||
* Get file information
|
||||
*/
|
||||
export async function getFileInfo(path: string): Promise<FileInfo> {
|
||||
export async function getFileInfo(path: string, opts: {
|
||||
skipSensitiveDetection: boolean;
|
||||
sensitiveThreshold?: number;
|
||||
sensitiveThresholdForPorn?: number;
|
||||
enableSensitiveMediaDetectionForVideos?: boolean;
|
||||
}): Promise<FileInfo> {
|
||||
const warnings = [] as string[];
|
||||
|
||||
const size = await getFileSize(path);
|
||||
|
@ -58,7 +71,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
|||
|
||||
// うまく判定できない画像は octet-stream にする
|
||||
if (!imageSize) {
|
||||
warnings.push(`cannot detect image dimensions`);
|
||||
warnings.push('cannot detect image dimensions');
|
||||
type = TYPE_OCTET_STREAM;
|
||||
} else if (imageSize.wUnits === 'px') {
|
||||
width = imageSize.width;
|
||||
|
@ -67,7 +80,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
|||
|
||||
// 制限を超えている画像は octet-stream にする
|
||||
if (imageSize.width > 16383 || imageSize.height > 16383) {
|
||||
warnings.push(`image dimensions exceeds limits`);
|
||||
warnings.push('image dimensions exceeds limits');
|
||||
type = TYPE_OCTET_STREAM;
|
||||
}
|
||||
} else {
|
||||
|
@ -84,6 +97,19 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
|||
});
|
||||
}
|
||||
|
||||
let sensitive = false;
|
||||
let porn = false;
|
||||
|
||||
if (!opts.skipSensitiveDetection) {
|
||||
[sensitive, porn] = await detectSensitivity(
|
||||
path,
|
||||
type.mime,
|
||||
opts.sensitiveThreshold ?? 0.5,
|
||||
opts.sensitiveThresholdForPorn ?? 0.75,
|
||||
opts.enableSensitiveMediaDetectionForVideos ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
size,
|
||||
md5,
|
||||
|
@ -92,10 +118,150 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
|||
height,
|
||||
orientation,
|
||||
blurhash,
|
||||
sensitive,
|
||||
porn,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
async function detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
|
||||
let sensitive = false;
|
||||
let porn = false;
|
||||
|
||||
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
|
||||
let sensitive = false;
|
||||
let porn = false;
|
||||
|
||||
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
|
||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
|
||||
|
||||
return [sensitive, porn];
|
||||
}
|
||||
|
||||
if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) {
|
||||
const result = await detectSensitive(source);
|
||||
if (result) {
|
||||
[sensitive, porn] = judgePrediction(result);
|
||||
}
|
||||
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
|
||||
const [outDir, disposeOutDir] = await createTempDir();
|
||||
try {
|
||||
const command = FFmpeg()
|
||||
.input(source)
|
||||
.inputOptions([
|
||||
'-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない)
|
||||
'-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない)
|
||||
])
|
||||
.noAudio()
|
||||
.videoFilters([
|
||||
{
|
||||
filter: 'select', // フレームのフィルタリング
|
||||
options: {
|
||||
e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい)
|
||||
},
|
||||
},
|
||||
{
|
||||
filter: 'blackframe', // 暗いフレームの検出
|
||||
options: {
|
||||
amount: '0', // 暗さに関わらず全てのフレームで測定値を取る
|
||||
},
|
||||
},
|
||||
{
|
||||
filter: 'metadata',
|
||||
options: {
|
||||
mode: 'select', // フレーム選択モード
|
||||
key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する)
|
||||
value: '50',
|
||||
function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので)
|
||||
},
|
||||
},
|
||||
{
|
||||
filter: 'scale',
|
||||
options: {
|
||||
w: 299,
|
||||
h: 299,
|
||||
},
|
||||
},
|
||||
])
|
||||
.format('image2')
|
||||
.output(join(outDir, '%d.png'))
|
||||
.outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない
|
||||
const results: ReturnType<typeof judgePrediction>[] = [];
|
||||
let frameIndex = 0;
|
||||
let targetIndex = 0;
|
||||
let nextIndex = 1;
|
||||
for await (const path of asyncIterateFrames(outDir, command)) {
|
||||
try {
|
||||
const index = frameIndex++;
|
||||
if (index !== targetIndex) {
|
||||
continue;
|
||||
}
|
||||
targetIndex = nextIndex;
|
||||
nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける
|
||||
const result = await detectSensitive(path);
|
||||
if (result) {
|
||||
results.push(judgePrediction(result));
|
||||
}
|
||||
} finally {
|
||||
fs.promises.unlink(path);
|
||||
}
|
||||
}
|
||||
sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold);
|
||||
porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn);
|
||||
} finally {
|
||||
disposeOutDir();
|
||||
}
|
||||
}
|
||||
|
||||
return [sensitive, porn];
|
||||
}
|
||||
|
||||
async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
|
||||
const watcher = new FSWatcher({
|
||||
cwd,
|
||||
disableGlobbing: true,
|
||||
});
|
||||
let finished = false;
|
||||
command.once('end', () => {
|
||||
finished = true;
|
||||
watcher.close();
|
||||
});
|
||||
command.run();
|
||||
for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
|
||||
const current = `${i}.png`;
|
||||
const next = `${i + 1}.png`;
|
||||
const framePath = join(cwd, current);
|
||||
if (await exists(join(cwd, next))) {
|
||||
yield framePath;
|
||||
} else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
|
||||
watcher.add(next);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
watcher.on('add', function onAdd(path) {
|
||||
if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている
|
||||
watcher.unwatch(current);
|
||||
watcher.off('add', onAdd);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている
|
||||
command.once('error', reject);
|
||||
});
|
||||
yield framePath;
|
||||
} else if (await exists(framePath)) {
|
||||
yield framePath;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exists(path: string): Promise<boolean> {
|
||||
return fs.promises.access(path).then(() => true, () => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME Type and extension
|
||||
*/
|
||||
|
|
|
@ -156,6 +156,19 @@ export class DriveFile {
|
|||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the DriveFile is NSFW. (predict)',
|
||||
})
|
||||
public maybeSensitive: boolean;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public maybePorn: boolean;
|
||||
|
||||
/**
|
||||
* 外部の(信頼されていない)URLへの直リンクか否か
|
||||
*/
|
||||
|
|
|
@ -188,6 +188,28 @@ export class Meta {
|
|||
})
|
||||
public recaptchaSecretKey: string | null;
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['none', 'all', 'local', 'remote'],
|
||||
default: 'none',
|
||||
})
|
||||
public sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote';
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
|
||||
default: 'medium',
|
||||
})
|
||||
public sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh';
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public setSensitiveFlagAutomatically: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableSensitiveMediaDetectionForVideos: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 1024,
|
||||
comment: 'Drive capacity of a local user (MB)',
|
||||
|
|
|
@ -152,6 +152,11 @@ export class UserProfile {
|
|||
})
|
||||
public alwaysMarkNsfw: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoSensitive: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -360,6 +360,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
|
||||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
||||
autoSensitive: profile!.autoSensitive,
|
||||
carefulBot: profile!.carefulBot,
|
||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||
noCrawle: profile!.noCrawle,
|
||||
|
|
|
@ -161,19 +161,19 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
maxLength: 4,
|
||||
value: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
maxLength: 4,
|
||||
},
|
||||
},
|
||||
followersCount: {
|
||||
|
@ -292,6 +292,10 @@ export const packedMeDetailedOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
autoSensitive: {
|
||||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
carefulBot: {
|
||||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
|
|
|
@ -195,6 +195,22 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
sensitiveMediaDetection: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
sensitiveMediaDetectionSensitivity: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
setSensitiveFlagAutomatically: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
enableSensitiveMediaDetectionForVideos: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
proxyAccountId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
|
@ -370,6 +386,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
blockedHosts: instance.blockedHosts,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
||||
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
|
||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
twitterConsumerKey: instance.twitterConsumerKey,
|
||||
twitterConsumerSecret: instance.twitterConsumerSecret,
|
||||
|
|
|
@ -58,6 +58,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
alwaysMarkNsfw: profile.alwaysMarkNsfw,
|
||||
autoSensitive: profile.autoSensitive,
|
||||
carefulBot: profile.carefulBot,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
|
|
|
@ -25,7 +25,7 @@ export const paramDef = {
|
|||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||
username: { type: 'string', nullable: true, default: null },
|
||||
hostname: {
|
||||
type: 'string',
|
||||
|
@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
|
||||
if (ps.hostname) {
|
||||
query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' });
|
||||
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
|
|
|
@ -48,6 +48,10 @@ export const paramDef = {
|
|||
enableRecaptcha: { type: 'boolean' },
|
||||
recaptchaSiteKey: { type: 'string', nullable: true },
|
||||
recaptchaSecretKey: { type: 'string', nullable: true },
|
||||
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
|
||||
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
||||
setSensitiveFlagAutomatically: { type: 'boolean' },
|
||||
enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
|
||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
maintainerName: { type: 'string', nullable: true },
|
||||
maintainerEmail: { type: 'string', nullable: true },
|
||||
|
@ -213,6 +217,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.recaptchaSecretKey = ps.recaptchaSecretKey;
|
||||
}
|
||||
|
||||
if (ps.sensitiveMediaDetection !== undefined) {
|
||||
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
|
||||
}
|
||||
|
||||
if (ps.sensitiveMediaDetectionSensitivity !== undefined) {
|
||||
set.sensitiveMediaDetectionSensitivity = ps.sensitiveMediaDetectionSensitivity;
|
||||
}
|
||||
|
||||
if (ps.setSensitiveFlagAutomatically !== undefined) {
|
||||
set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically;
|
||||
}
|
||||
|
||||
if (ps.enableSensitiveMediaDetectionForVideos !== undefined) {
|
||||
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
|
||||
}
|
||||
|
||||
if (ps.proxyAccountId !== undefined) {
|
||||
set.proxyAccountId = ps.proxyAccountId;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import ms from 'ms';
|
|||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import define from '../../../define.js';
|
||||
import { apiLogger } from '../../../logger.js';
|
||||
|
@ -35,6 +36,18 @@ export const meta = {
|
|||
code: 'INVALID_FILE_NAME',
|
||||
id: 'f449b209-0c60-4e51-84d5-29486263bfd4',
|
||||
},
|
||||
|
||||
inappropriate: {
|
||||
message: 'Cannot upload the file because it has been determined that it possibly contains inappropriate content.',
|
||||
code: 'INAPPROPRIATE',
|
||||
id: 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2',
|
||||
},
|
||||
|
||||
noFreeSpace: {
|
||||
message: 'Cannot upload the file because you have no free space of drive.',
|
||||
code: 'NO_FREE_SPACE',
|
||||
id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -87,6 +100,10 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, hea
|
|||
if (e instanceof Error || typeof e === 'string') {
|
||||
apiLogger.error(e);
|
||||
}
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
|
||||
if (e.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
|
||||
}
|
||||
throw new ApiError();
|
||||
} finally {
|
||||
cleanup!();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { publishDriveStream } from '@/services/stream.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { DriveFiles, DriveFolders, Users } from '@/models/index.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
|
|
@ -3,17 +3,17 @@ import * as mfm from 'mfm-js';
|
|||
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||
import acceptAllFollowRequests from '@/services/following/requests/accept-all.js';
|
||||
import { publishToFollowers } from '@/services/i/update.js';
|
||||
import define from '../../define.js';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import { updateUsertags } from '@/services/update-hashtag.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { notificationTypes } from '@/types.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { langmap } from '@/misc/langmap.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -57,7 +57,7 @@ export const meta = {
|
|||
message: 'Invalid Regular Expression.',
|
||||
code: 'INVALID_REGEXP',
|
||||
id: '0d786918-10df-41cd-8f33-8dec7d9a89a5',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
|
@ -77,7 +77,8 @@ export const paramDef = {
|
|||
lang: { type: 'string', enum: [null, ...Object.keys(langmap)], nullable: true },
|
||||
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
fields: { type: 'array',
|
||||
fields: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
maxItems: 16,
|
||||
items: {
|
||||
|
@ -102,6 +103,7 @@ export const paramDef = {
|
|||
injectFeaturedNote: { type: 'boolean' },
|
||||
receiveAnnouncementEmail: { type: 'boolean' },
|
||||
alwaysMarkNsfw: { type: 'boolean' },
|
||||
autoSensitive: { type: 'boolean' },
|
||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
pinnedPageId: { type: 'array', items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
|
@ -168,6 +170,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||
if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
||||
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||
|
||||
if (ps.avatarId) {
|
||||
|
|
|
@ -102,29 +102,169 @@
|
|||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function addStyle(styleText) {
|
||||
let css = document.createElement('style');
|
||||
css.appendChild(document.createTextNode(styleText));
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
|
||||
function renderError(code, details) {
|
||||
let errorsElement = document.getElementById('errors');
|
||||
|
||||
if (!errorsElement) {
|
||||
document.documentElement.innerHTML = `
|
||||
<h1>⚠ An error has occurred. ⚠</h1>
|
||||
<p>If the problem persists, please contact the administrator. You may also try the following options:</p>
|
||||
<ul>
|
||||
<li>Start <a href="/cli">the simple client</a></li>
|
||||
<li>Attempt to repair in <a href="/bios">BIOS</a></li>
|
||||
<li><a href="/flush">Flush preferences and cache</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
</svg>
|
||||
<h1>An error has occurred!</h1>
|
||||
<button class="button-big" onclick="location.reload(true);">
|
||||
<span class="button-label-big">Refresh</span>
|
||||
</button>
|
||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Clear preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the repair tool</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<div id="errors"></div>
|
||||
`;
|
||||
|
||||
errorsElement = document.getElementById('errors');
|
||||
}
|
||||
|
||||
const detailsElement = document.createElement('details');
|
||||
detailsElement.innerHTML = `<summary><code>ERROR CODE: ${code}</code></summary>${JSON.stringify(details)}`;
|
||||
|
||||
detailsElement.innerHTML = `
|
||||
<br>
|
||||
<summary>
|
||||
<code>ERROR CODE: ${code}</code>
|
||||
</summary>
|
||||
<code>${JSON.stringify(details)}</code>`;
|
||||
errorsElement.appendChild(detailsElement);
|
||||
addStyle(`
|
||||
* {
|
||||
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#misskey_app,
|
||||
#splash {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
background-color: #222;
|
||||
color: #dfddcc;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 999px;
|
||||
padding: 0px 12px 0px 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.button-big {
|
||||
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.button-big:hover {
|
||||
background: rgb(153, 204, 0);
|
||||
}
|
||||
|
||||
.button-small {
|
||||
background: #444;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.button-small:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.button-label-big {
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.button-label-small {
|
||||
color: rgb(153, 204, 0);
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(134, 179, 0);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dont-worry,
|
||||
#msg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
color: #dec340;
|
||||
height: 4rem;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Fira, FiraCode, monospace;
|
||||
}
|
||||
|
||||
details {
|
||||
background: #333;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
width: 40rem;
|
||||
border-radius: 10px;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
summary > * {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
details {
|
||||
width: 50%;
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
|
|
28
packages/backend/src/services/detect-sensitive.ts
Normal file
28
packages/backend/src/services/detect-sensitive.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as nsfw from 'nsfwjs';
|
||||
import * as tf from '@tensorflow/tfjs-node';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
let model: nsfw.NSFWJS;
|
||||
|
||||
export async function detectSensitive(path: string): Promise<nsfw.predictionType[] | null> {
|
||||
try {
|
||||
if (model == null) model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
||||
|
||||
const buffer = await fs.promises.readFile(path);
|
||||
const image = await tf.node.decodeImage(buffer, 3) as tf.Tensor3D;
|
||||
try {
|
||||
const predictions = await model.classify(image);
|
||||
return predictions;
|
||||
} finally {
|
||||
image.dispose();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/i
|
|||
import { genId } from '@/misc/gen-id.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { getS3 } from './s3.js';
|
||||
import { InternalStorage } from './internal-storage.js';
|
||||
import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js';
|
||||
|
@ -349,9 +350,31 @@ export async function addFile({
|
|||
requestIp = null,
|
||||
requestHeaders = null,
|
||||
}: AddFileArgs): Promise<DriveFile> {
|
||||
const info = await getFileInfo(path);
|
||||
let skipNsfwCheck = false;
|
||||
const instance = await fetchMeta();
|
||||
if (user == null) skipNsfwCheck = true;
|
||||
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'local' && Users.isRemoteUser(user)) skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'remote' && Users.isLocalUser(user)) skipNsfwCheck = true;
|
||||
|
||||
const info = await getFileInfo(path, {
|
||||
skipSensitiveDetection: skipNsfwCheck,
|
||||
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||
0.5,
|
||||
sensitiveThresholdForPorn: 0.75,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
});
|
||||
logger.info(`${JSON.stringify(info)}`);
|
||||
|
||||
// 現状 false positive が多すぎて実用に耐えない
|
||||
//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
|
||||
// throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
|
||||
//}
|
||||
|
||||
// detect name
|
||||
const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled');
|
||||
|
||||
|
@ -387,7 +410,7 @@ export async function addFile({
|
|||
// If usage limit exceeded
|
||||
if (usage + info.size > driveCapacity) {
|
||||
if (Users.isLocalUser(user)) {
|
||||
throw new Error('no-free-space');
|
||||
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
|
||||
} else {
|
||||
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
||||
deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser);
|
||||
|
@ -441,6 +464,8 @@ export async function addFile({
|
|||
file.isLink = isLink;
|
||||
file.requestIp = requestIp;
|
||||
file.requestHeaders = requestHeaders;
|
||||
file.maybeSensitive = info.sensitive;
|
||||
file.maybePorn = info.porn;
|
||||
file.isSensitive = user
|
||||
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
||||
(sensitive !== null && sensitive !== undefined)
|
||||
|
@ -448,6 +473,9 @@ export async function addFile({
|
|||
: false
|
||||
: false;
|
||||
|
||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
|
||||
if (url !== null) {
|
||||
file.src = url;
|
||||
|
||||
|
|
|
@ -10,9 +10,11 @@ const _dirname = dirname(_filename);
|
|||
describe('Get file info', () => {
|
||||
it('Empty file', async (async () => {
|
||||
const path = `${_dirname}/resources/emptyfile`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 0,
|
||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
|
@ -28,9 +30,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic JPEG', async (async () => {
|
||||
const path = `${_dirname}/resources/Lenna.jpg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 25360,
|
||||
md5: '091b3f259662aa31e2ffef4519951168',
|
||||
|
@ -46,9 +50,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic APNG', async (async () => {
|
||||
const path = `${_dirname}/resources/anime.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 1868,
|
||||
md5: '08189c607bea3b952704676bb3c979e0',
|
||||
|
@ -64,9 +70,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic AGIF', async (async () => {
|
||||
const path = `${_dirname}/resources/anime.gif`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 2248,
|
||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||
|
@ -82,9 +90,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('PNG with alpha', async (async () => {
|
||||
const path = `${_dirname}/resources/with-alpha.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 3772,
|
||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||
|
@ -100,9 +110,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic SVG', async (async () => {
|
||||
const path = `${_dirname}/resources/image.svg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 505,
|
||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||
|
@ -119,9 +131,11 @@ describe('Get file info', () => {
|
|||
it('SVG with XML definition', async (async () => {
|
||||
// https://github.com/misskey-dev/misskey/issues/4413
|
||||
const path = `${_dirname}/resources/with-xml-def.svg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 544,
|
||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||
|
@ -137,9 +151,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Dimension limit', async (async () => {
|
||||
const path = `${_dirname}/resources/25000x25000.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 75933,
|
||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||
|
@ -155,9 +171,11 @@ describe('Get file info', () => {
|
|||
|
||||
it('Rotate JPEG', async (async () => {
|
||||
const path = `${_dirname}/resources/rotate.jpg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 12624,
|
||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@
|
|||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "3.0.0-beta.0",
|
||||
"@vitejs/plugin-vue": "3.0.0-beta.1",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
|
@ -74,7 +74,7 @@
|
|||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vite": "3.0.0-beta.6",
|
||||
"vite": "3.0.0-beta.7",
|
||||
"vue": "3.2.37",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
|
|
|
@ -198,7 +198,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
height: 100%;
|
||||
background: var(--accent);
|
||||
opacity: 0.5;
|
||||
transition: width 0.2s cubic-bezier(0,0,0,1);
|
||||
//transition: width 0.2s cubic-bezier(0,0,0,1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +231,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
cursor: grab;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: left 0.2s cubic-bezier(0,0,0,1);
|
||||
//transition: left 0.2s cubic-bezier(0,0,0,1);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
|
|
|
@ -154,7 +154,7 @@ function createDoughnut(chartEl, tooltip, data) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
os.apiGet('federation/stats', { limit: 20 }).then(fedStats => {
|
||||
os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
|
||||
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
|
|
|
@ -26,12 +26,7 @@
|
|||
<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" class="visibility">
|
||||
<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
|
@ -43,12 +38,9 @@
|
|||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<span v-if="appearNote.visibility !== 'public'" class="visibility">
|
||||
<i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<i v-else-if="appearNote.visibility === 'specified'" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="appearNote.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
<div class="info">
|
||||
<MkVisibility :note="appearNote"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
||||
|
@ -134,6 +126,7 @@ import XPoll from './poll.vue';
|
|||
import XRenoteButton from './renote-button.vue';
|
||||
import MkUrlPreview from '@/components/url-preview.vue';
|
||||
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
||||
import MkVisibility from '@/components/visibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
@ -388,14 +381,6 @@ if (appearNote.replyId) {
|
|||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
> .visibility {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> .localOnly {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -441,6 +426,10 @@ if (appearNote.replyId) {
|
|||
border: solid 0.5px var(--divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
> .info {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,7 @@
|
|||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</MkA>
|
||||
<span v-if="note.visibility !== 'public'" class="visibility">
|
||||
<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
@ -22,6 +17,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkVisibility from '@/components/visibility.vue';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
||||
|
@ -74,14 +70,6 @@ defineProps<{
|
|||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .visibility {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> .localOnly {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,12 +28,7 @@
|
|||
<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" class="visibility">
|
||||
<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
|
@ -118,6 +113,7 @@ import XPoll from './poll.vue';
|
|||
import XRenoteButton from './renote-button.vue';
|
||||
import MkUrlPreview from '@/components/url-preview.vue';
|
||||
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
||||
import MkVisibility from '@/components/visibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
|
@ -406,14 +402,6 @@ function readPromo() {
|
|||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
> .visibility {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> .localOnly {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import XDetails from '@/components/renote.details.vue';
|
||||
import XDetails from '@/components/users-tooltip.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
|
47
packages/client/src/components/visibility.vue
Normal file
47
packages/client/src/components/visibility.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<span v-if="note.visibility !== 'public'" :class="$style.visibility">
|
||||
<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import XDetails from '@/components/users-tooltip.vue';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: {
|
||||
visibility: string;
|
||||
localOnly?: boolean;
|
||||
visibleUserIds?: string[];
|
||||
},
|
||||
}>();
|
||||
|
||||
const specified = $ref<HTMLElement>();
|
||||
|
||||
if (props.note.visibility === 'specified') {
|
||||
useTooltip($$(specified), async (showing) => {
|
||||
const users = await os.api('users/show', {
|
||||
userIds: props.note.visibleUserIds,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
users,
|
||||
count: props.note.visibleUserIds.length,
|
||||
targetElement: specified,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.visibility, .localOnly {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,5 @@
|
|||
import { computed, ref, reactive } from 'vue';
|
||||
import { $i } from './account';
|
||||
import { mainRouter } from '@/router';
|
||||
import { search } from '@/scripts/search';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@ -55,26 +54,7 @@ export const menuDef = reactive({
|
|||
title: 'lists',
|
||||
icon: 'fas fa-list-ul',
|
||||
show: computed(() => $i != null),
|
||||
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/list/') || mainRouter.currentRoute.value.path === '/my/lists' || mainRouter.currentRoute.value.path.startsWith('/my/lists/')),
|
||||
action: (ev) => {
|
||||
const items = ref([{
|
||||
type: 'pending',
|
||||
}]);
|
||||
os.api('users/lists/list').then(lists => {
|
||||
const _items = [...lists.map(list => ({
|
||||
type: 'link',
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`,
|
||||
})), null, {
|
||||
type: 'link',
|
||||
to: '/my/lists',
|
||||
text: i18n.ts.manageLists,
|
||||
icon: 'fas fa-cog',
|
||||
}];
|
||||
items.value = _items;
|
||||
});
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
to: '/my/lists',
|
||||
},
|
||||
/*
|
||||
groups: {
|
||||
|
@ -88,26 +68,7 @@ export const menuDef = reactive({
|
|||
title: 'antennas',
|
||||
icon: 'fas fa-satellite',
|
||||
show: computed(() => $i != null),
|
||||
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/antenna/') || mainRouter.currentRoute.value.path === '/my/antennas' || mainRouter.currentRoute.value.path.startsWith('/my/antennas/')),
|
||||
action: (ev) => {
|
||||
const items = ref([{
|
||||
type: 'pending',
|
||||
}]);
|
||||
os.api('antennas/list').then(antennas => {
|
||||
const _items = [...antennas.map(antenna => ({
|
||||
type: 'link',
|
||||
text: antenna.name,
|
||||
to: `/timeline/antenna/${antenna.id}`,
|
||||
})), null, {
|
||||
type: 'link',
|
||||
to: '/my/antennas',
|
||||
text: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-cog',
|
||||
}];
|
||||
items.value = _items;
|
||||
});
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
to: '/my/antennas',
|
||||
},
|
||||
favorites: {
|
||||
title: 'favorites',
|
||||
|
|
|
@ -14,6 +14,49 @@
|
|||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fas fa-eye-slash"></i></template>
|
||||
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
|
||||
<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
|
||||
<template v-else-if="sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
|
||||
<template v-else-if="sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }}</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<span class="_formBlock">{{ i18n.ts._sensitiveMediaDetection.description }}</span>
|
||||
|
||||
<FormRadios v-model="sensitiveMediaDetection" class="_formBlock">
|
||||
<option value="none">{{ i18n.ts.none }}</option>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.localOnly }}</option>
|
||||
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormSwitch v-model="enableSensitiveMediaDetectionForVideos" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="setSensitiveFlagAutomatically" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<!-- 現状 false positive が多すぎて実用に耐えない
|
||||
<FormSwitch v-model="disallowUploadWhenPredictedAsPorn" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
|
||||
</FormSwitch>
|
||||
-->
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Log IP address</template>
|
||||
<template v-if="enableIpLogging" #suffix>Enabled</template>
|
||||
|
@ -49,10 +92,11 @@ import { } from 'vue';
|
|||
import XBotProtection from './bot-protection.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormRange from '@/components/form/range.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
|
@ -63,6 +107,10 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
let summalyProxy: string = $ref('');
|
||||
let enableHcaptcha: boolean = $ref(false);
|
||||
let enableRecaptcha: boolean = $ref(false);
|
||||
let sensitiveMediaDetection: string = $ref('none');
|
||||
let sensitiveMediaDetectionSensitivity: number = $ref(0);
|
||||
let setSensitiveFlagAutomatically: boolean = $ref(false);
|
||||
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
|
||||
let enableIpLogging: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
|
@ -70,12 +118,31 @@ async function init() {
|
|||
summalyProxy = meta.summalyProxy;
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
sensitiveMediaDetection = meta.sensitiveMediaDetection;
|
||||
sensitiveMediaDetectionSensitivity =
|
||||
meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0;
|
||||
setSensitiveFlagAutomatically = meta.setSensitiveFlagAutomatically;
|
||||
enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
|
||||
enableIpLogging = meta.enableIpLogging;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy,
|
||||
sensitiveMediaDetection,
|
||||
sensitiveMediaDetectionSensitivity:
|
||||
sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' :
|
||||
sensitiveMediaDetectionSensitivity === 1 ? 'low' :
|
||||
sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
|
||||
sensitiveMediaDetectionSensitivity === 3 ? 'high' :
|
||||
sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
|
||||
0,
|
||||
setSensitiveFlagAutomatically,
|
||||
enableSensitiveMediaDetectionForVideos,
|
||||
enableIpLogging,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
|
|
|
@ -1,34 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
|
||||
<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
|
||||
<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<template #label>Moderation</template>
|
||||
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
|
||||
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
|
||||
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
|
||||
<MkButton @click="refreshMetadata"><i class="fas fa-refresh"></i> Refresh metadata</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
|
@ -56,8 +56,12 @@
|
|||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Open Registrations</template>
|
||||
<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
|
||||
<template #key>Following (Pub)</template>
|
||||
<template #value>{{ number(instance.followingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Followers (Sub)</template>
|
||||
<template #value>{{ number(instance.followersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
|
|
|
@ -28,7 +28,17 @@
|
|||
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
|
||||
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
|
||||
</FormLink>
|
||||
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
|
||||
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="alwaysMarkNsfw" class="_formBlock" @update:modelValue="saveProfile()">
|
||||
<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="autoSensitive" class="_formBlock" @update:modelValue="saveProfile()">
|
||||
<template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
<template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -47,11 +57,14 @@ import { defaultStore } from '@/store';
|
|||
import MkChart from '@/components/chart.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const fetching = ref(true);
|
||||
const usage = ref<any>(null);
|
||||
const capacity = ref<any>(null);
|
||||
const uploadFolder = ref<any>(null);
|
||||
let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw);
|
||||
let autoSensitive = $ref($i.autoSensitive);
|
||||
|
||||
const meterStyle = computed(() => {
|
||||
return {
|
||||
|
@ -94,6 +107,13 @@ function chooseUploadFolder() {
|
|||
});
|
||||
}
|
||||
|
||||
function saveProfile() {
|
||||
os.api('i/update', {
|
||||
alwaysMarkNsfw: !!alwaysMarkNsfw,
|
||||
autoSensitive: !!autoSensitive,
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
|
|
@ -56,8 +56,6 @@
|
|||
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
|
||||
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
|
||||
|
||||
<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -88,7 +86,6 @@ const profile = reactive({
|
|||
isBot: $i.isBot,
|
||||
isCat: $i.isCat,
|
||||
showTimelineReplies: $i.showTimelineReplies,
|
||||
alwaysMarkNsfw: $i.alwaysMarkNsfw,
|
||||
});
|
||||
|
||||
watch(() => profile, () => {
|
||||
|
@ -126,7 +123,6 @@ function save() {
|
|||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
showTimelineReplies: !!profile.showTimelineReplies,
|
||||
alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</FormSelect>
|
||||
|
||||
<MkInput v-model="statusbar.name" manual-save class="_formBlock">
|
||||
<template #label>Name</template>
|
||||
<template #label>{{ i18n.ts.label }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="statusbar.black" class="_formBlock">
|
||||
|
@ -16,7 +16,7 @@
|
|||
</MkSwitch>
|
||||
|
||||
<FormRadios v-model="statusbar.size" class="_formBlock">
|
||||
<template #label>Size</template>
|
||||
<template #label>{{ i18n.ts.size }}</template>
|
||||
<option value="verySmall">{{ i18n.ts.small }}+</option>
|
||||
<option value="small">{{ i18n.ts.small }}</option>
|
||||
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||
|
@ -29,27 +29,29 @@
|
|||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
|
||||
<template #label>Refresh interval</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
|
||||
<template #label>Duration</template>
|
||||
<template #label>{{ i18n.ts.refreshInterval }}</template>
|
||||
</MkInput>
|
||||
<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.speed }}</template>
|
||||
<template #caption>{{ i18n.ts.fast }} <-> {{ i18n.ts.slow }}</template>
|
||||
</FormRange>
|
||||
<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
|
||||
<template #label>Reverse</template>
|
||||
<template #label>{{ i18n.ts.reverse }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
<template v-else-if="statusbar.type === 'federation'">
|
||||
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
|
||||
<template #label>Refresh interval</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
|
||||
<template #label>Duration</template>
|
||||
<template #label>{{ i18n.ts.refreshInterval }}</template>
|
||||
</MkInput>
|
||||
<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.speed }}</template>
|
||||
<template #caption>{{ i18n.ts.fast }} <-> {{ i18n.ts.slow }}</template>
|
||||
</FormRange>
|
||||
<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
|
||||
<template #label>Reverse</template>
|
||||
<template #label>{{ i18n.ts.reverse }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="statusbar.props.colored" class="_formBlock">
|
||||
<template #label>Colored</template>
|
||||
<template #label>{{ i18n.ts.colored }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
<template v-else-if="statusbar.type === 'userList' && userLists != null">
|
||||
|
@ -58,18 +60,19 @@
|
|||
<option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
|
||||
</FormSelect>
|
||||
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
|
||||
<template #label>Refresh interval</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
|
||||
<template #label>Duration</template>
|
||||
<template #label>{{ i18n.ts.refreshInterval }}</template>
|
||||
</MkInput>
|
||||
<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.speed }}</template>
|
||||
<template #caption>{{ i18n.ts.fast }} <-> {{ i18n.ts.slow }}</template>
|
||||
</FormRange>
|
||||
<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
|
||||
<template #label>Reverse</template>
|
||||
<template #label>{{ i18n.ts.reverse }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<FormButton danger @click="del">Delete</FormButton>
|
||||
<FormButton danger @click="del">{{ i18n.ts.remove }}</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -81,6 +84,7 @@ import MkInput from '@/components/form/input.vue';
|
|||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormRange from '@/components/form/range.vue';
|
||||
import * as os from '@/os';
|
||||
import { menuDef } from '@/menu';
|
||||
import { defaultStore } from '@/store';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<template #suffix>{{ x.name }}</template>
|
||||
<XStatusbar :_id="x.id" :user-lists="userLists"/>
|
||||
</FormFolder>
|
||||
<FormButton @click="add">add</FormButton>
|
||||
<FormButton primary @click="add">{{ i18n.ts.add }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { ref } from 'vue';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
|
@ -20,10 +20,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err
|
||||
});
|
||||
// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
|
||||
});
|
||||
|
||||
// 一応廃棄
|
||||
|
@ -47,7 +44,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
type: 'url',
|
||||
placeholder: i18n.ts.uploadFromUrlDescription
|
||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
|
||||
|
@ -64,35 +61,35 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
os.api('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: defaultStore.state.uploadFolder,
|
||||
marker
|
||||
marker,
|
||||
});
|
||||
|
||||
os.alert({
|
||||
title: i18n.ts.uploadFromUrlRequested,
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
os.popupMenu([label ? {
|
||||
text: label,
|
||||
type: 'label'
|
||||
type: 'label',
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal
|
||||
ref: keepOriginal,
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'fas fa-upload',
|
||||
action: chooseFileFromPc
|
||||
action: chooseFileFromPc,
|
||||
}, {
|
||||
text: i18n.ts.fromDrive,
|
||||
icon: 'fas fa-cloud',
|
||||
action: chooseFileFromDrive
|
||||
action: chooseFileFromDrive,
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'fas fa-link',
|
||||
action: chooseFileFromUrl
|
||||
action: chooseFileFromUrl,
|
||||
}], src);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { defaultStore } from '@/store';
|
|||
import { apiUrl } from '@/config';
|
||||
import { $i } from '@/account';
|
||||
import { alert } from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
type Uploading = {
|
||||
id: string;
|
||||
|
@ -80,14 +81,37 @@ export function uploadFile(
|
|||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて再送できるようにしたい
|
||||
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'Failed to upload',
|
||||
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
|
||||
});
|
||||
if (ev.target?.response) {
|
||||
const res = JSON.parse(ev.target.response);
|
||||
if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseInappropriate,
|
||||
});
|
||||
} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
|
||||
});
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'Failed to upload',
|
||||
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
|
||||
});
|
||||
}
|
||||
|
||||
reject();
|
||||
return;
|
||||
|
|
|
@ -399,6 +399,16 @@ hr {
|
|||
}
|
||||
}
|
||||
|
||||
._beta {
|
||||
margin-left: 0.7em;
|
||||
font-size: 65%;
|
||||
padding: 2px 3px;
|
||||
color: var(--accent);
|
||||
border: solid 1px var(--accent);
|
||||
border-radius: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
._table {
|
||||
> ._row {
|
||||
display: flex;
|
||||
|
|
|
@ -80,7 +80,7 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
|||
::v-deep(.item) {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: 3em;
|
||||
margin-right: 5em;
|
||||
|
||||
> .icon {
|
||||
display: inline-block;
|
||||
|
|
|
@ -79,7 +79,7 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
|||
display: inline-block;
|
||||
width: 0.5px;
|
||||
height: var(--height);
|
||||
margin: 0 2em;
|
||||
margin: 0 3em;
|
||||
background: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
|||
display: inline-block;
|
||||
width: 0.5px;
|
||||
height: 16px;
|
||||
margin: 0 2em;
|
||||
margin: 0 3em;
|
||||
background: currentColor;
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,10 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
|
|||
padding: 0 var(--nameMargin);
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<XWidgets @mounted="attachSticky"/>
|
||||
</div>
|
||||
|
||||
<button class="widgetButton _button" :class="{ show: true }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
|
||||
<button v-if="!isDesktop && !isMobile" class="widgetButton _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
|
||||
|
||||
<div v-if="isMobile" class="buttons">
|
||||
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
|
@ -249,7 +249,6 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
> .widgetButton {
|
||||
display: block;
|
||||
position: fixed;
|
||||
|
@ -262,18 +261,6 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
|||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
|
||||
font-size: 22px;
|
||||
background: var(--panel);
|
||||
|
||||
&.navHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: ($widgets-hide-threshold + 1px)) {
|
||||
display: none;
|
||||
}
|
||||
}*/
|
||||
|
||||
> .widgetButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .widgetsDrawer-back {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-instance-cloud">
|
||||
<div class="">
|
||||
<MkTagCloud v-if="activeInstances">
|
||||
<li v-for="instance in activeInstances">
|
||||
<li v-for="instance in activeInstances" :key="instance.id">
|
||||
<a @click.prevent="onInstanceClick(instance)">
|
||||
<img style="width: 32px;" :src="instance.iconUrl">
|
||||
</a>
|
||||
|
|
|
@ -600,10 +600,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
|
||||
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
|
||||
|
||||
"@vitejs/plugin-vue@3.0.0-beta.0":
|
||||
version "3.0.0-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.0-beta.0.tgz#092f4f50ee183818e252331833541dbdcae1b91d"
|
||||
integrity sha512-t8os1QK1qpovpgYAJSOWYEu+Doy/DZRW1cNwMvUl0qo+Yv7D9a3cxo24oL01lbojcc9ABQhyvUP3BsvFNtriqg==
|
||||
"@vitejs/plugin-vue@3.0.0-beta.1":
|
||||
version "3.0.0-beta.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.0-beta.1.tgz#65a6be6ed619955a5edea6115dedcfc5da4ed3f6"
|
||||
integrity sha512-cPVQHIKZkVEQ8qW7+BlbTrGJXNpP2aMKzVhQdTnWK9u6cSDmVdZOXHmKPO2KVvrNpFXXS8R7hHBXMsSApA+XOA==
|
||||
|
||||
"@vue/compiler-core@3.2.37":
|
||||
version "3.2.37"
|
||||
|
@ -4215,10 +4215,10 @@ verror@1.10.0:
|
|||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vite@3.0.0-beta.6:
|
||||
version "3.0.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0-beta.6.tgz#dd54c304ce7ceca243be8a114f28c431bbc447a1"
|
||||
integrity sha512-jAxxCGXs6oIO3dFh7gwDEP9RqFzYY+ULDWawS1dd3HfM4FCr8rkOnLljDoBBIDdTNM8M7pDzdoYSmpPEOJqyZQ==
|
||||
vite@3.0.0-beta.7:
|
||||
version "3.0.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0-beta.7.tgz#ded6483ef3b9b16dbe3a912a35accb9cc3498530"
|
||||
integrity sha512-yjw154hB229qq5Bl6+/CJSTxC/yIDmDJbaAjE/pdracz3jytNEd2ovk5BvxgZT6+qPiUc2rRH3FgGqiZnweIFw==
|
||||
dependencies:
|
||||
esbuild "^0.14.47"
|
||||
postcss "^8.4.14"
|
||||
|
|
Loading…
Reference in a new issue