Merge branch 'develop'

This commit is contained in:
syuilo 2021-05-31 13:06:40 +09:00
commit de95bd3fb4
84 changed files with 966 additions and 855 deletions

View File

@ -6,10 +6,6 @@ And is distributed under The GNU Affero General Public License Version 3, you sh
Misskey includes several third-party Open-Source softwares.
Unicode emoji regular expressions by Twitter, Inc.
License: MIT
https://github.com/twitter/twemoji-parser/blob/master/LICENSE.md
Emoji keywords for Unicode 11 and below by Mu-An Chiou
License: MIT
https://github.com/muan/emojilib/blob/master/LICENSE

View File

@ -99,6 +99,11 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
To receive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
Related projects
----------------------------------------------------------------
- [misskey.js](https://github.com/misskey-dev/misskey.js) - Misskey SDK for JavaScript
- [mfm.js](https://github.com/misskey-dev/mfm.js) - MFM parser
:heart: Backers
----------------------------------------------------------------
<!-- PATREON_START -->

9
SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# Reporting Security Issues
If you discover a security issue in Misskey, please report it by sending an
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
This will allow us to assess the risk, and make a fix available before we add a
bug report to the GitHub repository.
Thanks for helping make Misskey safe for everyone.

BIN
assets/about/banner.svg (Stored with Git LFS)

Binary file not shown.

BIN
assets/banner.afdesign (Stored with Git LFS)

Binary file not shown.

View File

@ -4,7 +4,7 @@
import * as fs from 'fs';
import * as gulp from 'gulp';
import * as rimraf from 'rimraf';
import rimraf from 'rimraf';
const replace = require('gulp-replace');
const terser = require('gulp-terser');
const cssnano = require('gulp-cssnano');

View File

@ -259,8 +259,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "الصفحات"
integration: "دمج"
connectSerice: "أوصل"
disconnectSerice: "قطع الاتصال"
enableLocalTimeline: "تفعيل الخيط المحلي"
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دائمًا و من الوصول إلى جميع المخططات الزمنية ، حتى إذا لم يتم تمكينها."

View File

@ -269,8 +269,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Stránky"
integration: "Integrace"
connectSerice: "Připojit"
disconnectSerice: "Odpojit"
enableLocalTimeline: "Povolit lokální čas"
enableGlobalTimeline: "Povolit globální čas"
registration: "Registrace"

View File

@ -279,6 +279,7 @@ emptyDrive: "Drive ist leer"
emptyFolder: "Der Ordner ist leer"
unableToDelete: "Nicht löschbar"
inputNewFileName: "Gib einen neuen Dateinamen ein"
inputNewDescription: "Gib eine neue Beschreibung ein"
inputNewFolderName: "Gib einen neuen Ordnernamen ein"
circularReferenceFolder: "Der Zielordner ist ein Unterorder des Ordners, den du verschieben möchtest."
hasChildFilesOrFolders: "Dieser Ordner kann nicht gelöscht werden, da er nicht leer ist."
@ -310,8 +311,8 @@ monthX: "{month}"
yearX: "{year}"
pages: "Seiten"
integration: "Integration"
connectSerice: "Verbinden"
disconnectSerice: "Trennen"
connectService: "Verbinden"
disconnectService: "Trennen"
enableLocalTimeline: "Lokale Chronik aktivieren"
enableGlobalTimeline: "Globale Chronik aktivieren"
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind."
@ -546,6 +547,8 @@ disablePlayer: "Video-Player schließen"
expandTweet: "Tweet ausklappen"
themeEditor: "Farbthemen-Editor"
description: "Beschreibung"
describeFile: "Beschreibung hinzufügen"
enterFileDescription: "Beschreibung eingeben"
author: "Autor"
leaveConfirm: "Es gibt unspeicherte Änderungen. Möchtest du diese verwerfen?"
manage: "Verwaltung"

View File

@ -279,6 +279,7 @@ emptyDrive: "The drive is empty"
emptyFolder: "This folder is empty"
unableToDelete: "Unable to delete"
inputNewFileName: "Enter a new filename"
inputNewDescription: "Enter new caption"
inputNewFolderName: "Enter a new folder name"
circularReferenceFolder: "The destination folder is a subfolder of the folder you wish to move."
hasChildFilesOrFolders: "Since this folder is not empty, it can not be deleted."
@ -310,8 +311,8 @@ monthX: "{month}"
yearX: "{year} /"
pages: "Pages"
integration: "Integration"
connectSerice: "Connect"
disconnectSerice: "Disconnect"
connectService: "Connect"
disconnectService: "Disconnect"
enableLocalTimeline: "Enable local timeline"
enableGlobalTimeline: "Enable global timeline"
disablingTimelinesInfo: "Admins and Mods will always have access to all timelines, even if they are not enabled."
@ -546,6 +547,8 @@ disablePlayer: "Close video player"
expandTweet: "Expand tweet"
themeEditor: "Theme editor"
description: "Description"
describeFile: "Add caption"
enterFileDescription: "Enter caption"
author: "Author"
leaveConfirm: "There are unsaved changes. Do you want to discard them?"
manage: "Management"

View File

@ -309,8 +309,6 @@ monthX: "Mes {month}"
yearX: "Año {year}"
pages: "Páginas"
integration: "Integración"
connectSerice: "Conectarse"
disconnectSerice: "Desconectarse"
enableLocalTimeline: "Habilitar linea de tiempo local"
enableGlobalTimeline: "Habilitar linea de tiempo global"
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos"

View File

@ -310,8 +310,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Pages"
integration: "Intégrations"
connectSerice: "Connecter"
disconnectSerice: "Déconnecter"
enableLocalTimeline: "Activer le fil local"
enableGlobalTimeline: "Activer le fil global"
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s et les modérateur·rice·s pourront toujours y accéder."

View File

@ -1,5 +1,5 @@
---
_lang_: "Bahasa Jepang"
_lang_: "Bahasa Indonesia"
headlineMisskey: "Jaringan terhubung melalui note"
introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersifat sumber terbuka.\nMulailah menuliskan catatan, bagikan peristiwa terkini, serta ceritakan segala tentangmu.📡\nTunjukkan juga reaksimu pada catatan pengguna lain.👍\nMari jelajahi dunia baru🚀"
monthAndDay: "{day} {month}"
@ -310,8 +310,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Halaman"
integration: "Integrasi"
connectSerice: "Sambungkan"
disconnectSerice: "Putuskan"
enableLocalTimeline: "Nyalakan linimasa lokal"
enableGlobalTimeline: "Nyalakan linimasa global"
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa meskipun linimasa tersebut tidak diaktifkan."
@ -977,9 +975,9 @@ _theme:
infoFg: "Teks informasi"
infoWarnBg: "Latar belakang peringatan"
infoWarnFg: "Teks peringatan"
cwBg: "Latar belakang tombol CW"
cwFg: "Teks tombol CW"
cwHoverBg: "Latar belakang tombol CW (Mengambang)"
cwBg: "Latar belakang tombol Sembunyikan Konten"
cwFg: "Teks tombol Sembunyikan Konten"
cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)"
toastBg: "Latar belakang pemberitahuan"
toastFg: "Teks pemberitahuan"
buttonBg: "Latar belakang tombol"
@ -1122,7 +1120,7 @@ _widgets:
aiscript: "Konsol AiScript"
_cw:
hide: "Sembunyikan"
show: "Selebihnya"
show: "Lihat konten"
chars: "{count} karakter"
files: "{count} berkas"
_poll:
@ -1551,7 +1549,7 @@ _pages:
fn: "Fungsi"
_fn:
slots: "Slot"
slots-info: "Pisahkan setiap slow dengan baris baru"
slots-info: "Pisahkan setiap slot dengan baris baru"
arg1: "Keluaran"
for: "Ulangi"
_for:

View File

@ -21,6 +21,7 @@ const languages = [
'en-US',
'es-ES',
'fr-FR',
'id-ID',
'ja-JP',
'ja-KS',
'kab-KAB',

View File

@ -305,8 +305,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Pagine"
integration: "App collegate"
connectSerice: "Connetti"
disconnectSerice: "Disconnetti"
enableLocalTimeline: "Abilita Timeline locale"
enableGlobalTimeline: "Abilita Timeline federata"
disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori e i moderatori potranno sempre accederci."

View File

@ -279,6 +279,7 @@ emptyDrive: "ドライブは空です"
emptyFolder: "フォルダーは空です"
unableToDelete: "削除できません"
inputNewFileName: "新しいファイル名を入力してください"
inputNewDescription: "新しいキャプションを入力してください"
inputNewFolderName: "新しいフォルダ名を入力してください"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
hasChildFilesOrFolders: "このフォルダは空でないため、削除できません。"
@ -310,8 +311,8 @@ monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "連携"
connectSerice: "接続する"
disconnectSerice: "切断する"
connectService: "接続する"
disconnectService: "切断する"
enableLocalTimeline: "ローカルタイムラインを有効にする"
enableGlobalTimeline: "グローバルタイムラインを有効にする"
disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。"
@ -546,6 +547,8 @@ disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する"
themeEditor: "テーマエディター"
description: "説明"
describeFile: "キャプションを付ける"
enterFileDescription: "キャプションを入力"
author: "作者"
leaveConfirm: "未保存の変更があります。破棄しますか?"
manage: "管理"

View File

@ -308,8 +308,6 @@ monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "連携"
connectSerice: "つなぐ"
disconnectSerice: "切ってまう"
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"

View File

@ -279,6 +279,7 @@ emptyDrive: "드라이브가 비어 있습니다"
emptyFolder: "폴더가 비어 있습니다"
unableToDelete: "삭제할 수 없습니다"
inputNewFileName: "바꿀 파일명을 입력해 주세요"
inputNewDescription: "새 캡션을 입력해 주세요"
inputNewFolderName: "바꿀 폴더명을 입력해 주세요"
circularReferenceFolder: "지정한 폴더가 이동할 폴더의 하위 폴더입니다."
hasChildFilesOrFolders: "이 폴더는 비어있지 않기 때문에 삭제할 수 없습니다."
@ -310,8 +311,8 @@ monthX: "{month}월"
yearX: "{year}년"
pages: "페이지"
integration: "연동"
connectSerice: "접속"
disconnectSerice: "연결 끊기"
connectService: "계정 연동"
disconnectService: "계정 연동 해제"
enableLocalTimeline: "로컬 타임라인 활성화"
enableGlobalTimeline: "글로벌 타임라인 활성화"
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
@ -546,6 +547,8 @@ disablePlayer: "플레이어 닫기"
expandTweet: "트윗 확장하기"
themeEditor: "테마 에디터"
description: "설명"
describeFile: "캡션 추가"
enterFileDescription: "캡션 입력"
author: "작성자"
leaveConfirm: "저장하지 않은 변경사항이 있습니다. 취소하시겠습니까?"
manage: "관리"

View File

@ -135,6 +135,7 @@ settingGuide: "Proponowana konfiguracja"
cacheRemoteFiles: "Przechowuj zdalne pliki w pamięci podręcznej"
cacheRemoteFilesDescription: "Gdy ta opcja jest wyłączona, zdalne pliki są ładowane bezpośrednio ze zdalnych instancji. Wyłączenie the opcji zmniejszy użycie powierzchni dyskowej, ale zwiększy transfer, ponieważ miniaturki nie będą generowane."
flagAsBot: "To konto jest botem"
flagAsBotDescription: "Jeżeli ten kanał jest kontrolowany przez jakiś program, ustaw tę opcję. Jeżeli włączona, będzie działać jako flaga informująca innych programistów, aby zapobiegać nieskończonej interakcji z różnymi botami i dostosowywać wewnętrzne systemy Misskey, traktując konto jako bota."
flagAsCat: "To konto jest kotem"
flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot."
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
@ -182,6 +183,7 @@ clearQueueConfirmTitle: "Czy na pewno chcesz wyczyścić kolejkę?"
clearCachedFiles: "Wyczyść pamięć podręczną"
clearCachedFilesConfirm: "Czy na pewno chcesz usunąć wszystkie zdalne pliki z pamięci podręcznej?"
blockedInstances: "Zablokowane instancje"
blockedInstancesDescription: "Wypisz nazwy hostów instancji, które powinny zostać zablokowane. Wypisane instancje nie będą mogły dłużej komunikować się z tą instancją."
muteAndBlock: "Wycisz / Zablokuj"
mutedUsers: "Wyciszeni użytkownicy"
blockedUsers: "Zablokowani użytkownicy"
@ -274,6 +276,7 @@ emptyDrive: "Dysk jest pusty"
emptyFolder: "Ten katalog jest pusty"
unableToDelete: "Nie można usunąć"
inputNewFileName: "Wprowadź nową nazwę pliku"
inputNewDescription: "Proszę wpisać nowy napis"
inputNewFolderName: "Wprowadź nową nazwę katalogu"
circularReferenceFolder: "Katalog docelowy jest podkatalogiem katalogu, który chcesz przenieść."
hasChildFilesOrFolders: "Ponieważ ten katalog nie jest pusty, nie może być usunięty."
@ -305,8 +308,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Strony"
integration: "Integracja"
connectSerice: "Połącz"
disconnectSerice: "Rozłącz"
enableLocalTimeline: "Włącz lokalną oś czasu"
enableGlobalTimeline: "Włącz globalną oś czasu"
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do wszystkich osi czasu, nawet gdy są one wyłączone."
@ -532,6 +533,8 @@ disablePlayer: "Zamknij odtwarzacz wideo"
expandTweet: "Rozwiń tweet"
themeEditor: "Edytor motywu"
description: "Opis"
describeFile: "dodaj podpis"
enterFileDescription: "Wprowadź napis"
author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie"

View File

@ -309,8 +309,6 @@ monthX: "{month} месяц"
yearX: "{year} год"
pages: "Страницы"
integration: "Интеграция"
connectSerice: "Соединение"
disconnectSerice: "Отключение"
enableLocalTimeline: "Включить локальную ленту"
enableGlobalTimeline: "Включить глобальную ленту"
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, даже если они отключены."

View File

@ -307,8 +307,6 @@ monthX: "{month}"
yearX: "{year}"
pages: "Сторінки"
integration: "Інтеграція"
connectSerice: "Під’єднати"
disconnectSerice: "Відключитися"
enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті."

View File

@ -279,6 +279,7 @@ emptyDrive: "驱动器为空"
emptyFolder: "空文件夹"
unableToDelete: "无法删除"
inputNewFileName: "请输入新文件名"
inputNewDescription: "请输入新标题"
inputNewFolderName: "请输入新文件名"
circularReferenceFolder: "目标文件夹是您要移动的文件夹的子文件夹。"
hasChildFilesOrFolders: "此文件夹不为空,无法删除。"
@ -310,8 +311,8 @@ monthX: "{month}月"
yearX: "{year}年"
pages: "页面"
integration: "关联"
connectSerice: "连接"
disconnectSerice: "断开连接"
connectService: "连接"
disconnectService: "断开连接"
enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于便利性的原因,管理员和数据图表也可以继续使用。"
@ -546,6 +547,8 @@ disablePlayer: "关闭播放器"
expandTweet: "展开贴文"
themeEditor: "主题编辑器"
description: "描述"
describeFile: "添加标题"
enterFileDescription: "输入标题"
author: "作者"
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
manage: "管理"

View File

@ -1,18 +1,19 @@
---
_lang_: "繁體中文"
headlineMisskey: "貼文連繫網"
introMisskey: "歡迎! Misskey是一個開源且去中心化的社群網絡。\n通過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能對大家的貼文表達情感👍\n一起來探索這個新的世界吧🚀"
headlineMisskey: "貼文連繫網"
introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能對大家的貼文表達情感👍\n一起來探索這個新的世界吧🚀"
monthAndDay: "{month}月 {day}日"
search: "搜尋"
notifications: "通知"
username: "使用者名稱"
password: "密碼"
forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..."
ok: "OK"
gotIt: "知道了"
cancel: "取消"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了"
renotedBy: "{user} 轉了"
noNotes: "貼文不可用。"
noNotifications: "沒有通知"
instance: "實例"
@ -92,9 +93,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "轉成功"
renoted: "轉成功"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉發之前已經轉發過的內容。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
quote: "引用"
pinnedNote: "已置頂的貼文"
pinned: "置頂"
@ -309,8 +310,6 @@ monthX: "{month}月"
yearX: "{year}年"
pages: "頁面"
integration: "整合"
connectSerice: "連線"
disconnectSerice: "中斷連線"
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
@ -733,6 +732,7 @@ noBotProtectionWarning: "尚未設定Bot防護。"
configure: "設定"
expiration: "期限"
middle: "中"
emailNotConfiguredWarning: "沒有設定電子郵件地址"
_ad:
back: "返回"
_gallery:

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.81.2",
"version": "12.82.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -30,6 +30,7 @@
"format": "gulp format"
},
"resolutions": {
"mfm-js/twemoji-parser": "13.1.x",
"chokidar": "^3.3.1",
"constantinople": "^4.0.1",
"jsonld/rdf-canonize/node-forge": "0.10.0",
@ -43,7 +44,7 @@
"@koa/router": "9.0.1",
"@sentry/browser": "5.29.2",
"@sentry/tracing": "5.29.2",
"@sinonjs/fake-timers": "7.0.5",
"@sinonjs/fake-timers": "7.1.2",
"@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.1",
@ -58,23 +59,23 @@
"@types/jsdom": "16.2.10",
"@types/jsonld": "1.5.5",
"@types/katex": "0.11.0",
"@types/koa": "2.13.1",
"@types/koa": "2.13.3",
"@types/koa-bodyparser": "4.3.0",
"@types/koa-cors": "0.0.0",
"@types/koa-favicon": "2.0.19",
"@types/koa-logger": "3.1.1",
"@types/koa-mount": "4.0.0",
"@types/koa-send": "4.1.2",
"@types/koa-views": "2.0.4",
"@types/koa-views": "7.0.0",
"@types/koa__cors": "3.0.2",
"@types/koa__multer": "2.0.2",
"@types/koa__router": "8.0.4",
"@types/markdown-it": "12.0.1",
"@types/matter-js": "0.14.12",
"@types/mocha": "8.2.2",
"@types/node": "15.3.1",
"@types/node": "15.6.1",
"@types/node-fetch": "2.5.10",
"@types/nodemailer": "6.4.1",
"@types/nodemailer": "6.4.2",
"@types/nprogress": "0.2.0",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.0",
@ -85,12 +86,12 @@
"@types/qrcode": "1.4.0",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.1",
"@types/redis": "2.8.28",
"@types/redis": "2.8.29",
"@types/rename": "1.0.3",
"@types/request-stats": "3.0.0",
"@types/rimraf": "3.0.0",
"@types/seedrandom": "2.4.28",
"@types/sharp": "0.28.1",
"@types/sharp": "0.28.2",
"@types/sinonjs__fake-timers": "6.0.2",
"@types/speakeasy": "2.0.5",
"@types/throttle-debounce": "2.1.0",
@ -102,14 +103,14 @@
"@types/webpack-stream": "3.2.12",
"@types/websocket": "1.0.2",
"@types/ws": "7.4.4",
"@typescript-eslint/parser": "4.24.0",
"@typescript-eslint/parser": "4.25.0",
"@vue/compiler-sfc": "3.0.11",
"abort-controller": "3.0.0",
"apexcharts": "3.26.3",
"autobind-decorator": "2.4.0",
"autosize": "4.0.4",
"autwh": "0.1.0",
"aws-sdk": "2.910.0",
"aws-sdk": "2.918.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.3",
"broadcast-channel": "3.6.0",
@ -120,20 +121,20 @@
"chart.js": "2.9.4",
"cli-highlight": "2.1.11",
"commander": "7.2.0",
"concurrently": "6.1.0",
"concurrently": "6.2.0",
"content-disposition": "0.5.3",
"core-js": "3.12.1",
"core-js": "3.13.1",
"crc-32": "1.2.0",
"css-loader": "5.2.4",
"cssnano": "5.0.3",
"css-loader": "5.2.6",
"cssnano": "5.0.5",
"dateformat": "4.5.1",
"diskusage": "1.1.3",
"escape-regexp": "0.0.1",
"eslint": "7.26.0",
"eslint-plugin-vue": "7.9.0",
"eslint": "7.27.0",
"eslint-plugin-vue": "7.10.0",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
"file-type": "16.4.0",
"file-type": "16.5.0",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.7",
"got": "11.8.2",
@ -148,12 +149,12 @@
"http-proxy-agent": "4.0.1",
"http-signature": "1.3.5",
"https-proxy-agent": "5.0.0",
"idb-keyval": "5.0.5",
"idb-keyval": "5.0.6",
"insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0",
"is-svg": "4.3.1",
"js-yaml": "4.1.0",
"jsdom": "16.5.3",
"jsdom": "16.6.0",
"json5": "2.2.0",
"json5-loader": "4.0.1",
"jsonld": "4.0.1",
@ -174,22 +175,23 @@
"markdown-it-anchor": "7.1.0",
"matter-js": "0.17.1",
"mfm-js": "0.16.4",
"misskey-js": "0.0.2",
"mocha": "8.4.0",
"moji": "0.5.1",
"ms": "2.1.3",
"multer": "1.4.2",
"nested-property": "4.0.0",
"node-fetch": "2.6.1",
"nodemailer": "6.6.0",
"nodemailer": "6.6.1",
"object-assign-deep": "0.4.0",
"os-utils": "0.0.14",
"parse5": "6.0.1",
"pg": "8.6.0",
"portscanner": "2.2.0",
"postcss": "8.2.15",
"postcss": "8.3.0",
"postcss-loader": "5.3.0",
"prismjs": "1.23.0",
"probe-image-size": "7.1.0",
"probe-image-size": "7.1.1",
"promise-limit": "2.7.0",
"promise-sequential": "1.1.1",
"pug": "3.0.2",
@ -210,30 +212,31 @@
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.32.13",
"sass": "1.34.0",
"sass-loader": "11.1.1",
"seedrandom": "3.0.5",
"sharp": "0.28.2",
"sharp": "0.28.3",
"speakeasy": "2.0.0",
"stringz": "2.1.0",
"style-loader": "2.0.0",
"summaly": "2.4.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.6.22",
"systeminformation": "5.7.4",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.117.1",
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "9.2.1",
"ts-node": "9.1.1",
"ts-loader": "9.2.2",
"ts-node": "10.0.0",
"tsc-alias": "1.2.11",
"tsconfig-paths": "3.9.0",
"tslint": "6.1.3",
"tslint-sonarts": "1.9.0",
"twemoji-parser": "13.1.0",
"typeorm": "0.2.32",
"typescript": "4.2.4",
"typescript": "4.3.2",
"ulid": "2.3.0",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
@ -248,10 +251,10 @@
"vue-svg-loader": "0.17.0-beta.2",
"vuedraggable": "4.0.1",
"web-push": "3.4.4",
"webpack": "5.37.1",
"webpack": "5.38.1",
"webpack-cli": "4.7.0",
"websocket": "1.0.34",
"ws": "7.4.5",
"ws": "7.4.6",
"xev": "2.0.1"
},
"devDependencies": {

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { defineComponent, h, TransitionGroup } from 'vue';
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
import MkAd from '@client/components/global/ad.vue';
export default defineComponent({
props: {
items: {
type: Array,
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
required: true,
},
direction: {

View File

@ -87,6 +87,10 @@ export default defineComponent({
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: this.toggleSensitive
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: this.describe
}, null, {
text: this.$ts.copyUrl,
icon: 'fas fa-link',
@ -150,6 +154,26 @@ export default defineComponent({
});
},
describe() {
os.popup(import('@client/components/media-caption.vue'), {
title: this.$ts.describeFile,
input: {
placeholder: this.$ts.inputNewDescription,
default: this.file.comment !== null ? this.file.comment : '',
},
image: this.file
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result;
os.api('drive/files/update', {
fileId: this.file.id,
comment: comment.length == 0 ? null : comment
});
}
}, 'closed');
},
toggleSensitive() {
os.api('drive/files/update', {
fileId: this.file.id,

View File

@ -139,7 +139,7 @@ export default defineComponent({
});
}
this.connection = os.stream.useSharedConnection('drive');
this.connection = os.stream.useChannel('drive');
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
@ -301,7 +301,7 @@ export default defineComponent({
}
}).then(({ canceled, result: url }) => {
if (canceled) return;
os.api('drive/files/upload_from_url', {
os.api('drive/files/upload-from-url', {
url: url,
folderId: this.folder ? this.folder.id : undefined
});

View File

@ -71,7 +71,7 @@ export default defineComponent({
},
mounted() {
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);

View File

@ -2,7 +2,7 @@
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="xubzgfga">
<header>{{ image.name }}</header>
<img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/>
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
<footer>
<span>{{ image.type }}</span>
<span>{{ bytes(image.size) }}</span>

View File

@ -0,0 +1,238 @@
<template>
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
<div class="container">
<div class="fullwidth top-caption">
<div class="mk-dialog">
<header v-if="title"><Mfm :text="title"/></header>
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
<div class="buttons" v-if="(showOkButton || showCancelButton)">
<MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton>
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
</div>
</div>
</div>
<div class="hdrwpsaf fullwidth">
<header>{{ image.name }}</header>
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
<footer>
<span>{{ image.type }}</span>
<span>{{ bytes(image.size) }}</span>
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
</footer>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkModal from '@client/components/ui/modal.vue';
import MkButton from '@client/components/ui/button.vue';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
export default defineComponent({
components: {
MkModal,
MkButton,
},
props: {
image: {
type: Object,
required: true,
},
title: {
type: String,
required: false
},
input: {
required: true
},
showOkButton: {
type: Boolean,
default: true
},
showCancelButton: {
type: Boolean,
default: true
},
cancelableByBgClick: {
type: Boolean,
default: true
},
},
emits: ['done', 'closed'],
data() {
return {
inputValue: this.input.default ? this.input.default : null
};
},
mounted() {
document.addEventListener('keydown', this.onKeydown);
},
beforeUnmount() {
document.removeEventListener('keydown', this.onKeydown);
},
methods: {
bytes,
number,
done(canceled, result?) {
this.$emit('done', { canceled, result });
this.$refs.modal.close();
},
async ok() {
if (!this.showOkButton) return;
const result = this.inputValue;
this.done(false, result);
},
cancel() {
this.done(true);
},
onBgClick() {
if (this.cancelableByBgClick) {
this.cancel();
}
},
onKeydown(e) {
if (e.which === 27) { // ESC
this.cancel();
}
},
onInputKeydown(e) {
if (e.which === 13) { // Enter
if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
this.ok();
}
}
}
}
});
</script>
<style lang="scss" scoped>
.container {
display: flex;
width: 100%;
height: 100%;
flex-direction: row;
}
@media (max-width: 850px) {
.container {
flex-direction: column;
}
.top-caption {
padding-bottom: 8px;
}
}
.fullwidth {
width: 100%;
margin: auto;
}
.mk-dialog {
position: relative;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
text-align: center;
background: var(--panel);
border-radius: var(--radius);
margin: auto;
> header {
margin: 0 0 8px 0;
font-weight: bold;
font-size: 20px;
}
> .buttons {
margin-top: 16px;
> * {
margin: 0 8px;
}
}
> textarea {
display: block;
box-sizing: border-box;
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: inherit;
max-width: 100%;
min-width: 100%;
min-height: 90px;
&:focus {
outline: none;
}
&:disabled {
opacity: 0.5;
}
}
}
.hdrwpsaf {
display: flex;
flex-direction: column;
height: 100%;
> header,
> footer {
align-self: center;
display: inline-block;
padding: 6px 9px;
font-size: 90%;
background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
color: #fff;
}
> header {
margin-bottom: 8px;
opacity: 0.9;
}
> img {
display: block;
flex: 1;
min-height: 0;
object-fit: contain;
width: 100%;
cursor: zoom-out;
image-orientation: from-image;
}
> footer {
margin-top: 8px;
opacity: 0.8;
> span + span {
margin-left: 0.5em;
padding-left: 0.5em;
border-left: solid 1px rgba(255, 255, 255, 0.5);
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="qjewsnkg" v-if="hide" @click="hide = false">
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div class="text">
<div>
<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
@ -14,7 +14,7 @@
:title="image.name"
@click.prevent="onClick"
>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
</a>
<i class="fas fa-eye-slash" @click="hide = true"></i>

View File

@ -109,7 +109,7 @@ export default defineComponent({
this.readObserver.observe(this.$el);
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
}
},

View File

@ -12,10 +12,10 @@
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</XList>
<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<MkButton primary style="margin: var(--margin) auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
</MkButton>
</div>
</transition>
</template>
@ -28,12 +28,14 @@ import XList from './date-separated-list.vue';
import XNote from './note.vue';
import { notificationTypes } from '../../types';
import * as os from '@client/os';
import MkButton from '@client/components/ui/button.vue';
export default defineComponent({
components: {
XNotification,
XList,
XNote,
MkButton,
},
mixins: [
@ -87,7 +89,7 @@ export default defineComponent({
},
mounted() {
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('notification', this.onNotification);
},

View File

@ -89,6 +89,27 @@ export default defineComponent({
file.name = result;
});
},
async describe(file) {
os.popup(import("@client/components/media-caption.vue"), {
title: this.$ts.describeFile,
input: {
placeholder: this.$ts.inputNewDescription,
default: file.comment !== null ? file.comment : "",
},
image: file
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result;
os.api('drive/files/update', {
fileId: file.id,
comment: comment.length == 0 ? null : comment
});
}
}, 'closed');
},
showFileMenu(file, ev: MouseEvent) {
if (this.menu) return;
this.menu = os.modalMenu([{
@ -99,6 +120,10 @@ export default defineComponent({
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
action: () => { this.toggleSensitive(file) }
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: () => { this.describe(file) }
}, {
text: this.$ts.attachCancel,
icon: 'fas fa-times-circle',

View File

@ -92,33 +92,33 @@ export default defineComponent({
this.query = {
antennaId: this.antenna
};
this.connection = os.stream.connectToChannel('antenna', {
this.connection = os.stream.useChannel('antenna', {
antennaId: this.antenna
});
this.connection.on('note', prepend);
} else if (this.src == 'home') {
endpoint = 'notes/timeline';
this.connection = os.stream.useSharedConnection('homeTimeline');
this.connection = os.stream.useChannel('homeTimeline');
this.connection.on('note', prepend);
this.connection2 = os.stream.useSharedConnection('main');
this.connection2 = os.stream.useChannel('main');
this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') {
endpoint = 'notes/local-timeline';
this.connection = os.stream.useSharedConnection('localTimeline');
this.connection = os.stream.useChannel('localTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'social') {
endpoint = 'notes/hybrid-timeline';
this.connection = os.stream.useSharedConnection('hybridTimeline');
this.connection = os.stream.useChannel('hybridTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'global') {
endpoint = 'notes/global-timeline';
this.connection = os.stream.useSharedConnection('globalTimeline');
this.connection = os.stream.useChannel('globalTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'mentions') {
endpoint = 'notes/mentions';
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('mention', prepend);
} else if (this.src == 'directs') {
endpoint = 'notes/mentions';
@ -130,14 +130,14 @@ export default defineComponent({
prepend(note);
}
};
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('mention', onNote);
} else if (this.src == 'list') {
endpoint = 'notes/user-list-timeline';
this.query = {
listId: this.list
};
this.connection = os.stream.connectToChannel('userList', {
this.connection = os.stream.useChannel('userList', {
listId: this.list
});
this.connection.on('note', prepend);
@ -148,7 +148,7 @@ export default defineComponent({
this.query = {
channelId: this.channel
};
this.connection = os.stream.connectToChannel('channel', {
this.connection = os.stream.useChannel('channel', {
channelId: this.channel
});
this.connection.on('note', prepend);

View File

@ -163,8 +163,6 @@ fetchInstance().then(() => {
initializeSw();
});
stream.init($i);
const app = createApp(await (
window.location.search === '?zen' ? import('@client/ui/zen.vue') :
!$i ? import('@client/ui/visitor.vue') :
@ -296,7 +294,7 @@ if ($i) {
}
}
const main = stream.useSharedConnection('main', 'System');
const main = stream.useChannel('main', null, 'System');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
@ -358,10 +356,6 @@ if ($i) {
sound.play('channel');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {

View File

@ -1,26 +1,14 @@
import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js';
import { api } from './os';
// TODO: 他のタブと永続化されたstateを同期
export type Instance = {
emojis: {
category: string;
}[];
ads: {
id: string;
ratio: number;
place: string;
url: string;
imageUrl: string;
}[];
};
const data = localStorage.getItem('instance');
// TODO: instanceをリアクティブにするかは再考の余地あり
export const instance: Instance = reactive(data ? JSON.parse(data) : {
export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : {
// TODO: set default values
});

View File

@ -3,16 +3,16 @@
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
import * as Sentry from '@sentry/browser';
import Stream from '@client/scripts/stream';
import { apiUrl, debug } from '@client/config';
import { apiUrl, debug, url } from '@client/config';
import MkPostFormDialog from '@client/components/post-form-dialog.vue';
import MkWaitingDialog from '@client/components/waiting-dialog.vue';
import { resolve } from '@client/router';
import { $i } from '@client/account';
import { defaultStore } from '@client/store';
export const stream = markRaw(new Stream());
export const stream = markRaw(new Misskey.Stream(url, $i));
export const pendingApiRequestsCount = ref(0);
let apiRequestsCount = 0; // for debug
@ -20,7 +20,11 @@ export const apiRequests = ref([]); // for debug
export const windows = new Map();
export function api(endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) {
const apiClient = new Misskey.api.APIClient({
origin: url,
});
export const api = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
pendingApiRequestsCount.value++;
const onFinally = () => {
@ -56,7 +60,7 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
if (res.status === 200) {
resolve(body);
if (debug) {
log!.res = markRaw(body);
log!.res = markRaw(JSON.parse(JSON.stringify(body)));
log!.state = 'success';
}
} else if (res.status === 204) {
@ -90,17 +94,15 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
promise.then(onFinally, onFinally);
return promise;
}
}) as typeof apiClient.request;
export function apiWithDialog(
export const apiWithDialog = ((
endpoint: string,
data: Record<string, any> = {},
token?: string | null | undefined,
onSuccess?: (res: any) => void,
onFailure?: (e: Error) => void,
) {
) => {
const promise = api(endpoint, data, token);
promiseDialog(promise, onSuccess, onFailure ? onFailure : (e) => {
promiseDialog(promise, null, (e) => {
dialog({
type: 'error',
text: e.message + '\n' + (e as any).id,
@ -108,7 +110,7 @@ export function apiWithDialog(
});
return promise;
}
}) as typeof api;
export function promiseDialog<T extends Promise<any>>(
promise: T,

View File

@ -90,7 +90,7 @@ export default defineComponent({
stats: null,
serverInfo: null,
connection: null,
queueConnection: os.stream.useSharedConnection('queueStats'),
queueConnection: os.stream.useChannel('queueStats'),
memUsage: 0,
chartCpuMem: null,
chartNet: null,
@ -121,7 +121,7 @@ export default defineComponent({
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = os.stream.useSharedConnection('serverStats');
this.connection = os.stream.useChannel('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {

View File

@ -92,6 +92,7 @@ export default defineComponent({
version,
url,
stats: null,
meta: null,
fetchStats: () => os.api('stats', {}),
fetchServerInfo: () => os.api('admin/server-info', {}),
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),

View File

@ -35,7 +35,7 @@ export default defineComponent({
title: this.$ts.jobQueue,
icon: 'fas fa-clipboard-list',
},
connection: os.stream.useSharedConnection('queueStats'),
connection: os.stream.useChannel('queueStats'),
}
},

View File

@ -63,7 +63,7 @@ export default defineComponent({
},
mounted() {
this.connection = os.stream.useSharedConnection('messagingIndex');
this.connection = os.stream.useChannel('messagingIndex');
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);

View File

@ -141,7 +141,7 @@ const Component = defineComponent({
this.group = group;
}
this.connection = os.stream.connectToChannel('messaging', {
this.connection = os.stream.useChannel('messaging', {
otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined,
});

View File

@ -61,7 +61,7 @@ export default defineComponent({
if (this.connection) {
this.connection.dispose();
}
this.connection = os.stream.connectToChannel('gamesReversiGame', {
this.connection = os.stream.useChannel('gamesReversiGame', {
gameId: this.game.id
});
this.connection.on('started', this.onStarted);

View File

@ -92,7 +92,7 @@ export default defineComponent({
mounted() {
if (this.$i) {
this.connection = os.stream.useSharedConnection('gamesReversi');
this.connection = os.stream.useChannel('gamesReversi');
this.connection.on('invited', this.onInvited);

View File

@ -4,8 +4,8 @@
<div class="_formLabel"><i class="fab fa-twitter"></i> Twitter</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectSerice }}</MkButton>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
</div>
</div>
@ -13,8 +13,8 @@
<div class="_formLabel"><i class="fab fa-discord"></i> Discord</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectSerice }}</MkButton>
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
</div>
</div>
@ -22,8 +22,8 @@
<div class="_formLabel"><i class="fab fa-github"></i> GitHub</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectSerice }}</MkButton>
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
</div>
</div>
</FormBase>

View File

@ -3,6 +3,11 @@ import * as url from '../../prelude/url';
export function getStaticImageUrl(baseUrl: string): string {
const u = new URL(baseUrl);
if (u.href.startsWith(`${instanceUrl}/proxy/`)) {
// もう既にproxyっぽそうだったらsearchParams付けるだけ
u.searchParams.set('static', '1');
return u.href;
}
const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので
return `${instanceUrl}/proxy/${dummy}?${url.query({
url: u.href,

View File

@ -47,7 +47,7 @@ export function selectFile(src: any, label: string | null, multiple = false) {
const marker = Math.random().toString(); // TODO: UUIDとか使う
const connection = os.stream.useSharedConnection('main');
const connection = os.stream.useChannel('main');
connection.on('urlUploadFinished', data => {
if (data.marker === marker) {
res(multiple ? [data.file] : data.file);
@ -55,7 +55,7 @@ export function selectFile(src: any, label: string | null, multiple = false) {
}
});
os.api('drive/files/upload_from_url', {
os.api('drive/files/upload-from-url', {
url: url,
marker
});

View File

@ -1,312 +0,0 @@
import autobind from 'autobind-decorator';
import { EventEmitter } from 'eventemitter3';
import ReconnectingWebsocket from 'reconnecting-websocket';
import { markRaw } from 'vue';
import { debug, wsUrl } from '@client/config';
import { query as urlQuery } from '../../prelude/url';
/**
* Misskey stream connection
*/
export default class Stream extends EventEmitter {
private stream: ReconnectingWebsocket;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = [];
@autobind
public init(user): void {
const query = urlQuery({
i: user?.token,
_t: Date.now(),
});
this.stream = new ReconnectingWebsocket(`${wsUrl}?${query}`, '', { minReconnectionDelay: 1 }); // https://github.com/pladaria/reconnecting-websocket/issues/91
this.stream.addEventListener('open', this.onOpen);
this.stream.addEventListener('close', this.onClose);
this.stream.addEventListener('message', this.onMessage);
}
@autobind
public useSharedConnection(channel: string, name?: string): SharedConnection {
let pool = this.sharedConnectionPools.find(p => p.channel === channel);
if (pool == null) {
pool = new Pool(this, channel);
this.sharedConnectionPools.push(pool);
}
const connection = markRaw(new SharedConnection(this, channel, pool, name));
this.sharedConnections.push(connection);
return connection;
}
@autobind
public removeSharedConnection(connection: SharedConnection) {
this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
}
@autobind
public removeSharedConnectionPool(pool: Pool) {
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
}
@autobind
public connectToChannel(channel: string, params?: any): NonSharedConnection {
const connection = markRaw(new NonSharedConnection(this, channel, params));
this.nonSharedConnections.push(connection);
return connection;
}
@autobind
public disconnectToChannel(connection: NonSharedConnection) {
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection);
}
/**
* Callback of when open connection
*/
@autobind
private onOpen() {
const isReconnect = this.state === 'reconnecting';
this.state = 'connected';
this.emit('_connected_');
// チャンネル再接続
if (isReconnect) {
for (const p of this.sharedConnectionPools)
p.connect();
for (const c of this.nonSharedConnections)
c.connect();
}
}
/**
* Callback of when close connection
*/
@autobind
private onClose() {
if (this.state === 'connected') {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
}
/**
* Callback of when received a message from connection
*/
@autobind
private onMessage(message) {
const { type, body } = JSON.parse(message.data);
if (type === 'channel') {
const id = body.id;
let connections: Connection[];
connections = this.sharedConnections.filter(c => c.id === id);
if (connections.length === 0) {
connections = [this.nonSharedConnections.find(c => c.id === id)];
}
for (const c of connections.filter(c => c != null)) {
c.emit(body.type, Object.freeze(body.body));
if (debug) c.inCount++;
}
} else {
this.emit(type, Object.freeze(body));
}
}
/**
* Send a message to connection
*/
@autobind
public send(typeOrPayload, payload?) {
const data = payload === undefined ? typeOrPayload : {
type: typeOrPayload,
body: payload
};
this.stream.send(JSON.stringify(data));
}
/**
* Close this connection
*/
@autobind
public close() {
this.stream.removeEventListener('open', this.onOpen);
this.stream.removeEventListener('message', this.onMessage);
}
}
let idCounter = 0;
class Pool {
public channel: string;
public id: string;
protected stream: Stream;
public users = 0;
private disposeTimerId: any;
private isConnected = false;
constructor(stream: Stream, channel: string) {
this.channel = channel;
this.stream = stream;
this.id = (++idCounter).toString();
this.stream.on('_disconnected_', this.onStreamDisconnected);
}
@autobind
private onStreamDisconnected() {
this.isConnected = false;
}
@autobind
public inc() {
if (this.users === 0 && !this.isConnected) {
this.connect();
}
this.users++;
// タイマー解除
if (this.disposeTimerId) {
clearTimeout(this.disposeTimerId);
this.disposeTimerId = null;
}
}
@autobind
public dec() {
this.users--;
// そのコネクションの利用者が誰もいなくなったら
if (this.users === 0) {
// また直ぐに再利用される可能性があるので、一定時間待ち、
// 新たな利用者が現れなければコネクションを切断する
this.disposeTimerId = setTimeout(() => {
this.disconnect();
}, 3000);
}
}
@autobind
public connect() {
if (this.isConnected) return;
this.isConnected = true;
this.stream.send('connect', {
channel: this.channel,
id: this.id
});
}
@autobind
private disconnect() {
this.stream.off('_disconnected_', this.onStreamDisconnected);
this.stream.send('disconnect', { id: this.id });
this.stream.removeSharedConnectionPool(this);
}
}
abstract class Connection extends EventEmitter {
public channel: string;
protected stream: Stream;
public abstract id: string;
public name?: string; // for debug
public inCount: number = 0; // for debug
public outCount: number = 0; // for debug
constructor(stream: Stream, channel: string, name?: string) {
super();
this.stream = stream;
this.channel = channel;
this.name = name;
}
@autobind
public send(id: string, typeOrPayload, payload?) {
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
const body = payload === undefined ? typeOrPayload.body : payload;
this.stream.send('ch', {
id: id,
type: type,
body: body
});
if (debug) this.outCount++;
}
public abstract dispose(): void;
}
class SharedConnection extends Connection {
private pool: Pool;
public get id(): string {
return this.pool.id;
}
constructor(stream: Stream, channel: string, pool: Pool, name?: string) {
super(stream, channel, name);
this.pool = pool;
this.pool.inc();
}
@autobind
public send(typeOrPayload, payload?) {
super.send(this.pool.id, typeOrPayload, payload);
}
@autobind
public dispose() {
this.pool.dec();
this.removeAllListeners();
this.stream.removeSharedConnection(this);
}
}
class NonSharedConnection extends Connection {
public id: string;
protected params: any;
constructor(stream: Stream, channel: string, params?: any) {
super(stream, channel);
this.params = params;
this.id = (++idCounter).toString();
this.connect();
}
@autobind
public connect() {
this.stream.send('connect', {
channel: this.channel,
id: this.id,
params: this.params
});
}
@autobind
public send(typeOrPayload, payload?) {
super.send(this.id, typeOrPayload, payload);
}
@autobind
public dispose() {
this.removeAllListeners();
this.stream.send('disconnect', { id: this.id });
this.stream.disconnectToChannel(this);
}
}

View File

@ -146,6 +146,7 @@ hr {
width: 100%;
height: 100%;
background: var(--modalBg);
-webkit-backdrop-filter: var(--modalBgFilter);
backdrop-filter: var(--modalBgFilter);
}

View File

@ -43,7 +43,7 @@ export default defineComponent({
};
if ($i) {
const connection = stream.useSharedConnection('main', 'UI');
const connection = stream.useChannel('main', null, 'UI');
connection.on('notification', onNotification);
}

View File

@ -121,33 +121,33 @@ export default defineComponent({
this.query = {
antennaId: this.antenna
};
this.connection = os.stream.connectToChannel('antenna', {
this.connection = os.stream.useChannel('antenna', {
antennaId: this.antenna
});
this.connection.on('note', prepend);
} else if (this.src == 'home') {
endpoint = 'notes/timeline';
this.connection = os.stream.useSharedConnection('homeTimeline');
this.connection = os.stream.useChannel('homeTimeline');
this.connection.on('note', prepend);
this.connection2 = os.stream.useSharedConnection('main');
this.connection2 = os.stream.useChannel('main');
this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') {
endpoint = 'notes/local-timeline';
this.connection = os.stream.useSharedConnection('localTimeline');
this.connection = os.stream.useChannel('localTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'social') {
endpoint = 'notes/hybrid-timeline';
this.connection = os.stream.useSharedConnection('hybridTimeline');
this.connection = os.stream.useChannel('hybridTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'global') {
endpoint = 'notes/global-timeline';
this.connection = os.stream.useSharedConnection('globalTimeline');
this.connection = os.stream.useChannel('globalTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'mentions') {
endpoint = 'notes/mentions';
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('mention', prepend);
} else if (this.src == 'directs') {
endpoint = 'notes/mentions';
@ -159,14 +159,14 @@ export default defineComponent({
prepend(note);
}
};
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('mention', onNote);
} else if (this.src == 'list') {
endpoint = 'notes/user-list-timeline';
this.query = {
listId: this.list
};
this.connection = os.stream.connectToChannel('userList', {
this.connection = os.stream.useChannel('userList', {
listId: this.list
});
this.connection.on('note', prepend);
@ -178,7 +178,7 @@ export default defineComponent({
this.query = {
channelId: this.channel
};
this.connection = os.stream.connectToChannel('channel', {
this.connection = os.stream.useChannel('channel', {
channelId: this.channel
});
this.connection.on('note', prepend);

View File

@ -241,7 +241,6 @@ export default defineComponent({
> .text {
display: none;
}
}
}
@ -309,7 +308,7 @@ export default defineComponent({
> .indicator {
position: absolute;
top: 0;
left: 20px;
left: 0;
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;

View File

@ -65,7 +65,7 @@ export default defineComponent({
extends: widget,
data() {
return {
connection: os.stream.useSharedConnection('queueStats'),
connection: os.stream.useChannel('queueStats'),
inbox: {
activeSincePrevTick: 0,
active: 0,

View File

@ -48,7 +48,7 @@ export default defineComponent({
};
},
mounted() {
this.connection = os.stream.useSharedConnection('main');
this.connection = os.stream.useChannel('main');
this.connection.on('driveFileCreated', this.onDriveFileCreated);

View File

@ -63,7 +63,7 @@ export default defineComponent({
os.api('server-info', {}).then(res => {
this.meta = res;
});
this.connection = os.stream.useSharedConnection('serverStats');
this.connection = os.stream.useChannel('serverStats');
},
unmounted() {
this.connection.dispose();

View File

@ -93,6 +93,9 @@
{ "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] },
{ "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] },
{ "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] },
{ "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] },
{ "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] },
{ "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] },
{ "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] },
{ "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] },
{ "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] },
@ -1219,6 +1222,8 @@
{ "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] },
{ "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] },
{ "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] },
{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] },
{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] },
{ "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] },
{ "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] },
{ "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] },

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,8 @@ import { Note } from '../models/entities/note';
import { Cache } from './cache';
import { isSelfHost, toPunyNullable } from './convert-host';
import { decodeReaction } from './reaction-lib';
import config from '@/config';
import { query } from '@/prelude/url';
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
@ -59,9 +61,12 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
if (emoji == null) return null;
const isLocal = emoji.host == null;
const url = isLocal ? emoji.url : `${config.url}/proxy/image.png?${query({url: emoji.url})}`;
return {
name: emojiName,
url: emoji.url,
url,
};
}

View File

@ -59,6 +59,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userId = :id', { id: id })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
@ -69,6 +70,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost = :host', { host: toPuny(host) })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
@ -79,6 +81,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
@ -89,6 +92,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NOT NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();

View File

@ -1,12 +1,12 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user';
import acceptFollow from './follow';
import { IAccept, IFollow } from '../../type';
import { IAccept, isFollow, getApType } from '../../type';
import { apLogger } from '../../logger';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
const uri = activity.id || activity;
logger.info(`Accept: ${uri}`);
@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
throw e;
});
switch (object.type) {
case 'Follow':
acceptFollow(actor, object as IFollow);
break;
if (isFollow(object)) return await acceptFollow(actor, object);
default:
logger.warn(`Unknown accept type: ${object.type}`);
break;
}
return `skip: Unknown Accept type: ${getApType(object)}`;
};

View File

@ -1,7 +1,7 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user';
import createNote from './note';
import { ICreate, getApId, validPost } from '../../type';
import { ICreate, getApId, isPost, getApType } from '../../type';
import { apLogger } from '../../logger';
import { toArray, concat, unique } from '../../../../prelude/array';
@ -35,9 +35,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
throw e;
});
if (validPost.includes(object.type)) {
if (isPost(object)) {
createNote(resolver, actor, object, false, activity);
} else {
logger.warn(`Unknown type: ${object.type}`);
logger.warn(`Unknown type: ${getApType(object)}`);
}
};

View File

@ -1,12 +1,12 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user';
import rejectFollow from './follow';
import { IReject, IFollow } from '../../type';
import { IReject, isFollow, getApType } from '../../type';
import { apLogger } from '../../logger';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
const uri = activity.id || activity;
logger.info(`Reject: ${uri}`);
@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
throw e;
});
switch (object.type) {
case 'Follow':
rejectFollow(actor, object as IFollow);
break;
if (isFollow(object)) return await rejectFollow(actor, object);
default:
logger.warn(`Unknown reject type: ${object.type}`);
break;
}
return `skip: Unknown Reject type: ${getApType(object)}`;
};

View File

@ -3,14 +3,15 @@ import { IRemoteUser } from '../../../../models/entities/user';
import { IAnnounce, getApId } from '../../type';
import deleteNote from '../../../../services/note/delete';
export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
const uri = getApId(activity);
const note = await Notes.findOne({
uri
});
if (!note) return;
if (!note) return 'skip: no such Announce';
await deleteNote(actor, note);
return 'ok: deleted';
};

View File

@ -1,5 +1,5 @@
import { IRemoteUser } from '../../../../models/entities/user';
import { IUndo, IFollow, IBlock, ILike, IAnnounce } from '../../type';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
import unfollow from './follow';
import unblock from './block';
import undoLike from './like';
@ -9,7 +9,7 @@ import { apLogger } from '../../logger';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
@ -25,20 +25,10 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
throw e;
});
switch (object.type) {
case 'Follow':
unfollow(actor, object as IFollow);
break;
case 'Block':
unblock(actor, object as IBlock);
break;
case 'Like':
case 'EmojiReaction':
case 'EmojiReact':
undoLike(actor, object as ILike);
break;
case 'Announce':
undoAnnounce(actor, object as IAnnounce);
break;
}
if (isFollow(object)) return await unfollow(actor, object);
if (isBlock(object)) return await unblock(actor, object);
if (isLike(object)) return await undoLike(actor, object);
if (isAnnounce(object)) return await undoAnnounce(actor, object);
return `skip: unknown object type ${getApType(object)}`;
};

View File

@ -1,5 +1,5 @@
import { IRemoteUser } from '../../../../models/entities/user';
import { IUpdate, validActor } from '../../type';
import { getApType, IUpdate, isActor } from '../../type';
import { apLogger } from '../../logger';
import { updateQuestion } from '../../models/question';
import Resolver from '../../resolver';
@ -22,13 +22,13 @@ export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> =>
throw e;
});
if (validActor.includes(object.type)) {
if (isActor(object)) {
await updatePerson(actor.uri!, resolver, object);
return `ok: Person updated`;
} else if (object.type === 'Question') {
} else if (getApType(object) === 'Question') {
await updateQuestion(object).catch(e => console.log(e));
return `ok: Question updated`;
} else {
return `skip: Unknown type: ${object.type}`;
return `skip: Unknown type: ${getApType(object)}`;
}
};

View File

@ -28,7 +28,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
const instance = await fetchMeta();
const cache = instance.cacheRemoteFiles;
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, image.name);
if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、

View File

@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import { extractDbHost, toPuny } from '@/misc/convert-host';
import { Emojis, Polls, MessagingMessages } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '@/misc/gen-id';
import { fetchMeta } from '@/misc/fetch-meta';
@ -36,8 +36,8 @@ export function validateNote(object: any, uri: string) {
return new Error('invalid Note: object is null');
}
if (!validPost.includes(object.type)) {
return new Error(`invalid Note: invalid object type ${object.type}`);
if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
}
if (object.id && extractDbHost(object.id) !== expectHost) {

View File

@ -4,7 +4,7 @@ import * as promiseLimit from 'promise-limit';
import config from '@/config';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue } from '../type';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType } from '../type';
import { fromHtml } from '../../../mfm/from-html';
import { htmlToMfm } from '../misc/html-to-mfm';
import { resolveNote, extractEmojis } from './note';
@ -137,7 +137,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const isBot = object.type === 'Service';
const isBot = getApType(object) === 'Service';
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@ -337,7 +337,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
emojis: emojiNames,
name: person.name,
tags,
isBot: object.type === 'Service',
isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
isExplorable: !!person.discoverable,
@ -476,7 +476,7 @@ export async function updateFeatured(userId: User['id']) {
// Resolve and regist Notes
const limit = promiseLimit<Note | null>(2);
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
.slice(0, 5)
.map(item => limit(() => resolveNote(item, resolver))));

View File

@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
export default (file: DriveFile) => ({
type: 'Document',
mediaType: file.type,
url: DriveFiles.getPublicUrl(file)
url: DriveFiles.getPublicUrl(file),
name: file.comment,
});

View File

@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
export default (file: DriveFile) => ({
type: 'Image',
url: DriveFiles.getPublicUrl(file),
sensitive: file.isSensitive
sensitive: file.isSensitive,
name: file.comment
});

View File

@ -3,7 +3,7 @@ export type ApObject = IObject | string | (IObject | string)[];
export interface IObject {
'@context': string | obj | obj[];
type: string;
type: string | unknown[];
id?: string;
summary?: string;
published?: string;
@ -51,6 +51,15 @@ export function getApId(value: string | IObject): string {
throw new Error(`cannot detemine id`);
}
/**
* Get ActivityStreams Object type
*/
export function getApType(value: IObject): string {
if (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error(`cannot detect type`);
}
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
const firstOne = Array.isArray(value) ? value[0] : value;
return getApHrefNullable(firstOne);
@ -92,6 +101,9 @@ export interface IOrderedCollection extends IObject {
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
export const isPost = (object: IObject): object is IPost =>
validPost.includes(getApType(object));
export interface IPost extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
_misskey_content?: string;
@ -112,7 +124,7 @@ export interface IQuestion extends IObject {
}
export const isQuestion = (object: IObject): object is IQuestion =>
object.type === 'Note' || object.type === 'Question';
getApType(object) === 'Note' || getApType(object) === 'Question';
interface IQuestionChoice {
name?: string;
@ -126,10 +138,13 @@ export interface ITombstone extends IObject {
}
export const isTombstone = (object: IObject): object is ITombstone =>
object.type === 'Tombstone';
getApType(object) === 'Tombstone';
export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
export const isActor = (object: IObject): object is IPerson =>
validActor.includes(getApType(object));
export interface IPerson extends IObject {
type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
name?: string;
@ -154,10 +169,10 @@ export interface IPerson extends IObject {
}
export const isCollection = (object: IObject): object is ICollection =>
object.type === 'Collection';
getApType(object) === 'Collection';
export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
object.type === 'OrderedCollection';
getApType(object) === 'OrderedCollection';
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
isCollection(object) || isOrderedCollection(object);
@ -171,7 +186,7 @@ export interface IApPropertyValue extends IObject {
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object &&
object.type === 'PropertyValue' &&
getApType(object) === 'PropertyValue' &&
typeof object.name === 'string' &&
typeof (object as any).value === 'string';
@ -181,7 +196,7 @@ export interface IApMention extends IObject {
}
export const isMention = (object: IObject): object is IApMention=>
object.type === 'Mention' &&
getApType(object) === 'Mention' &&
typeof object.href === 'string';
export interface IApHashtag extends IObject {
@ -190,7 +205,7 @@ export interface IApHashtag extends IObject {
}
export const isHashtag = (object: IObject): object is IApHashtag =>
object.type === 'Hashtag' &&
getApType(object) === 'Hashtag' &&
typeof object.name === 'string';
export interface IApEmoji extends IObject {
@ -199,7 +214,7 @@ export interface IApEmoji extends IObject {
}
export const isEmoji = (object: IObject): object is IApEmoji =>
object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
export interface ICreate extends IActivity {
type: 'Create';
@ -258,17 +273,17 @@ export interface IFlag extends IActivity {
type: 'Flag';
}
export const isCreate = (object: IObject): object is ICreate => object.type === 'Create';
export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete';
export const isUpdate = (object: IObject): object is IUpdate => object.type === 'Update';
export const isRead = (object: IObject): object is IRead => object.type === 'Read';
export const isUndo = (object: IObject): object is IUndo => object.type === 'Undo';
export const isFollow = (object: IObject): object is IFollow => object.type === 'Follow';
export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept';
export const isReject = (object: IObject): object is IReject => object.type === 'Reject';
export const isAdd = (object: IObject): object is IAdd => object.type === 'Add';
export const isRemove = (object: IObject): object is IRemove => object.type === 'Remove';
export const isLike = (object: IObject): object is ILike => object.type === 'Like' || object.type === 'EmojiReaction' || object.type === 'EmojiReact';
export const isAnnounce = (object: IObject): object is IAnnounce => object.type === 'Announce';
export const isBlock = (object: IObject): object is IBlock => object.type === 'Block';
export const isFlag = (object: IObject): object is IFlag => object.type === 'Flag';
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';

View File

@ -5,6 +5,8 @@ import { ApiError } from './error';
import { SchemaType } from '@/misc/schema';
import { AccessToken } from '../../models/entities/access-token';
type NonOptional<T> = T extends undefined ? never : T;
type SimpleUserInfo = {
id: ILocalUser['id'];
host: ILocalUser['host'];
@ -17,10 +19,11 @@ type SimpleUserInfo = {
isSilenced: ILocalUser['isSilenced'];
};
// TODO: defaultが設定されている場合はその型も考慮する
type Params<T extends IEndpointMeta> = {
[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
? ReturnType<NonNullable<T['params']>[P]['transform']>
: NonNullable<T['params']>[P]['default'] extends null | number | string
? NonOptional<ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]>
: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0];
};

View File

@ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { User } from '../../../../models/entities/user';
import { fetchMeta } from '@/misc/fetch-meta';
import { validActor, validPost } from '../../../../remote/activitypub/type';
import { isActor, isPost, getApId } from '../../../../remote/activitypub/type';
export const meta = {
tags: ['federation'],
@ -154,16 +154,16 @@ async function fetchAny(uri: string) {
}
// それでもみつからなければ新規であるため登録
if (validActor.includes(object.type)) {
const user = await createPerson(object.id);
if (isActor(object)) {
const user = await createPerson(getApId(object));
return {
type: 'User',
object: await Users.pack(user, null, { detail: true })
};
}
if (validPost.includes(object.type)) {
const note = await createNote(object.id, undefined, true);
if (isPost(object)) {
const note = await createNote(getApId(object), undefined, true);
return {
type: 'Note',
object: await Notes.pack(note!, null, { detail: true })

View File

@ -49,6 +49,14 @@ export const meta = {
'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
'en-US': 'Whether this media is NSFW'
}
},
comment: {
validator: $.optional.nullable.str,
default: undefined as any,
desc: {
'ja-JP': 'コメント'
}
}
},
@ -92,6 +100,8 @@ export default define(meta, async (ps, user) => {
if (ps.name) file.name = ps.name;
if (ps.comment !== undefined) file.comment = ps.comment;
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
if (ps.folderId !== undefined) {
@ -113,6 +123,7 @@ export default define(meta, async (ps, user) => {
await DriveFiles.update(file.id, {
name: file.name,
comment: file.comment,
folderId: file.folderId,
isSensitive: file.isSensitive
});

View File

@ -6,6 +6,7 @@ import { DriveFiles, GalleryPosts } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
import { GalleryPost } from '../../../../../models/entities/gallery-post';
import { ApiError } from '../../../error';
import { DriveFile } from '@/models/entities/drive-file';
export const meta = {
tags: ['gallery'],
@ -55,7 +56,7 @@ export default define(meta, async (ps, user) => {
id: fileId,
userId: user.id
})
))).filter(file => file != null);
))).filter((file): file is DriveFile => file != null);
if (files.length === 0) {
throw new Error();

View File

@ -5,6 +5,7 @@ import { ID } from '../../../../../misc/cafy-id';
import { DriveFiles, GalleryPosts } from '../../../../../models';
import { GalleryPost } from '../../../../../models/entities/gallery-post';
import { ApiError } from '../../../error';
import { DriveFile } from '@/models/entities/drive-file';
export const meta = {
tags: ['gallery'],
@ -58,7 +59,7 @@ export default define(meta, async (ps, user) => {
id: fileId,
userId: user.id
})
))).filter(file => file != null);
))).filter((file): file is DriveFile => file != null);
if (files.length === 0) {
throw new Error();

View File

@ -5,6 +5,7 @@ import define from '../../define';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notifications, Followings, Mutings, Users } from '../../../../models';
import { notificationTypes } from '../../../../types';
import read from '@/services/note/read';
export const meta = {
desc: {
@ -103,9 +104,9 @@ export default define(meta, async (ps, user) => {
query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes?.length > 0) {
if (ps.includeTypes && ps.includeTypes.length > 0) {
query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes?.length > 0) {
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
}
@ -116,5 +117,11 @@ export default define(meta, async (ps, user) => {
readNotification(user.id, notifications.map(x => x.id));
}
const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
if (notes.length > 0) {
read(user.id, notes);
}
return await Notifications.packMany(notifications, user.id);
});

View File

@ -104,23 +104,26 @@ export default define(meta, async (ps, me) => {
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
try {
if (ps.tag) {
if (!safeForSql(ps.tag)) return;
if (!safeForSql(ps.tag)) throw 'Injection';
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
let i = 0;
query.andWhere(new Brackets(qb => {
for (const tags of ps.query!) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
if (!safeForSql(tag)) return;
qb.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
i++;
if (!safeForSql(tag)) throw 'Injection';
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
}
}));
}
}));
}
} catch (e) {
if (e === 'Injection') return [];
throw e;
}
if (ps.reply != null) {
if (ps.reply) {

View File

@ -93,7 +93,7 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
private static convertFlattenColumnsToObject(x: Record<string, number>) {
private static convertFlattenColumnsToObject(x: Record<string, any>): Record<string, any> {
const obj = {} as any;
for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
// now k is ___x_y_z
@ -285,8 +285,7 @@ export default abstract class Chart<T extends Record<string, any>> {
const latest = await this.getLatestLog(group);
if (latest != null) {
const obj = Chart.convertFlattenColumnsToObject(
latest as Record<string, any>);
const obj = Chart.convertFlattenColumnsToObject(latest) as T;
// 空ログデータを作成
data = this.getNewLog(obj);
@ -474,13 +473,13 @@ export default abstract class Chart<T extends Record<string, any>> {
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
if (log) {
const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
chart.unshift(Chart.countUniqueFields(data));
const data = Chart.convertFlattenColumnsToObject(log);
chart.unshift(Chart.countUniqueFields(data) as T);
} else {
// 隙間埋め
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)));
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
}
}
} else if (span === 'day') {
@ -497,14 +496,14 @@ export default abstract class Chart<T extends Record<string, any>> {
if (log) {
if (logsForEachDays[currentDayIndex]) {
logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log));
logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log) as T);
} else {
logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log)];
logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log) as T];
}
} else {
// 隙間埋め
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
const newLog = this.getNewLog(data);
if (logsForEachDays[currentDayIndex]) {
logsForEachDays[currentDayIndex].unshift(newLog);
@ -516,7 +515,7 @@ export default abstract class Chart<T extends Record<string, any>> {
for (const logs of logsForEachDays) {
const log = this.aggregate(logs);
chart.unshift(Chart.countUniqueFields(log));
chart.unshift(Chart.countUniqueFields(log) as T);
}
}

View File

@ -267,7 +267,8 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
async function deleteOldFile(user: IRemoteUser) {
const q = DriveFiles.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id });
.where('file.userId = :userId', { userId: user.id })
.andWhere('file.isLink = FALSE');
if (user.avatarId) {
q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });

View File

@ -79,7 +79,7 @@ async function postProcess(file: DriveFile, isExpired = false) {
url: file.uri,
thumbnailUrl: null,
webpublicUrl: null,
size: 0,
storedInternal: false,
// ローカルプロキシ用
accessKey: uuid(),
thumbnailAccessKey: 'thumbnail-' + uuid(),

View File

@ -25,6 +25,12 @@ export default async (
name = null;
}
// If the comment is same as the name, skip comment
// (image.name is passed in when receiving attachment)
if (comment !== null && name == comment) {
comment = null;
}
// Create temp file
const [path, cleanup] = await createTemp();

591
yarn.lock

File diff suppressed because it is too large Load Diff