Merge branch 'develop'
This commit is contained in:
		
						commit
						929e545514
					
				
					 84 changed files with 968 additions and 862 deletions
				
			
		
							
								
								
									
										4
									
								
								COPYING
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								COPYING
									
										
									
									
									
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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
									
								
							
							
						
						
									
										9
									
								
								SECURITY.md
									
										
									
									
									
										Normal 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. | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB | 
										
											Binary file not shown.
										
									
								
							|  | @ -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'); | ||||
|  |  | |||
|  | @ -259,8 +259,6 @@ monthX: "{month}" | |||
| yearX: "{year}" | ||||
| pages: "الصفحات" | ||||
| integration: "دمج" | ||||
| connectSerice: "أوصل" | ||||
| disconnectSerice: "قطع الاتصال" | ||||
| enableLocalTimeline: "تفعيل الخيط المحلي" | ||||
| enableGlobalTimeline: "تفعيل الخيط الزمني الشامل" | ||||
| disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دائمًا و من الوصول إلى جميع المخططات الزمنية ، حتى إذا لم يتم تمكينها." | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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." | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ const languages = [ | |||
| 	'en-US', | ||||
| 	'es-ES', | ||||
| 	'fr-FR', | ||||
| 	'id-ID', | ||||
| 	'ja-JP', | ||||
| 	'ja-KS', | ||||
| 	'kab-KAB', | ||||
|  |  | |||
|  | @ -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." | ||||
|  |  | |||
|  | @ -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: "管理" | ||||
|  |  | |||
|  | @ -308,8 +308,6 @@ monthX: "{month}月" | |||
| yearX: "{year}年" | ||||
| pages: "ページ" | ||||
| integration: "連携" | ||||
| connectSerice: "つなぐ" | ||||
| disconnectSerice: "切ってまう" | ||||
| enableLocalTimeline: "ローカルタイムラインを使えるようにする" | ||||
| enableGlobalTimeline: "グローバルタイムラインを使えるようにする" | ||||
| disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。" | ||||
|  |  | |||
|  | @ -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: "관리" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -309,8 +309,6 @@ monthX: "{month} месяц" | |||
| yearX: "{year} год" | ||||
| pages: "Страницы" | ||||
| integration: "Интеграция" | ||||
| connectSerice: "Соединение" | ||||
| disconnectSerice: "Отключение" | ||||
| enableLocalTimeline: "Включить локальную ленту" | ||||
| enableGlobalTimeline: "Включить глобальную ленту" | ||||
| disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, даже если они отключены." | ||||
|  |  | |||
|  | @ -307,8 +307,6 @@ monthX: "{month}" | |||
| yearX: "{year}" | ||||
| pages: "Сторінки" | ||||
| integration: "Інтеграція" | ||||
| connectSerice: "Під’єднати" | ||||
| disconnectSerice: "Відключитися" | ||||
| enableLocalTimeline: "Увімкнути локальну стрічку" | ||||
| enableGlobalTimeline: "Увімкнути глобальну стрічку" | ||||
| disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті." | ||||
|  |  | |||
|  | @ -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: "管理" | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
							
								
								
									
										63
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										63
									
								
								package.json
									
										
									
									
									
								
							|  | @ -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": { | ||||
|  |  | |||
|  | @ -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: { | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 				}); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										238
									
								
								src/client/components/media-caption.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/client/components/media-caption.vue
									
										
									
									
									
										Normal 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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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)); | ||||
| 		} | ||||
| 	}, | ||||
|  |  | |||
|  | @ -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); | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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', () => { | ||||
|  |  | |||
|  | @ -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
 | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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', { | ||||
|  |  | |||
|  | @ -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', {}), | ||||
|  |  | |||
|  | @ -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'), | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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, | ||||
| 			}); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 				}); | ||||
|  |  | |||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -146,6 +146,7 @@ hr { | |||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	background: var(--modalBg); | ||||
| 	-webkit-backdrop-filter: var(--modalBgFilter); | ||||
| 	backdrop-filter: var(--modalBgFilter); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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
											
										
									
								
							|  | @ -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, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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)}`; | ||||
| }; | ||||
|  |  | |||
|  | @ -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)}`); | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -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)}`; | ||||
| }; | ||||
|  |  | |||
|  | @ -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'; | ||||
| }; | ||||
|  |  | |||
|  | @ -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)}`; | ||||
| }; | ||||
|  |  | |||
|  | @ -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)}`; | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -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で登録されていたということなので、
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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)))); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| }); | ||||
|  |  | |||
|  | @ -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 | ||||
| }); | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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,11 +19,12 @@ 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']> | ||||
| 		: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]; | ||||
| 		: 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]; | ||||
| }; | ||||
| 
 | ||||
| export type Response = Record<string, any> | void; | ||||
|  |  | |||
|  | @ -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 }) | ||||
|  |  | |||
|  | @ -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 | ||||
| 	}); | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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); | ||||
| }); | ||||
|  |  | |||
|  | @ -104,22 +104,25 @@ export default define(meta, async (ps, me) => { | |||
| 	generateVisibilityQuery(query, me); | ||||
| 	if (me) generateMutedUserQuery(query, me); | ||||
| 
 | ||||
| 	if (ps.tag) { | ||||
| 		if (!safeForSql(ps.tag)) return; | ||||
| 		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++; | ||||
| 					} | ||||
| 				})); | ||||
| 			} | ||||
| 		})); | ||||
| 	try { | ||||
| 		if (ps.tag) { | ||||
| 			if (!safeForSql(ps.tag)) throw 'Injection'; | ||||
| 			query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); | ||||
| 		} else { | ||||
| 			query.andWhere(new Brackets(qb => { | ||||
| 				for (const tags of ps.query!) { | ||||
| 					qb.orWhere(new Brackets(qb => { | ||||
| 						for (const tag of tags) { | ||||
| 							if (!safeForSql(tag)) throw 'Injection'; | ||||
| 							qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); | ||||
| 						} | ||||
| 					})); | ||||
| 				} | ||||
| 			})); | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		if (e === 'Injection') return []; | ||||
| 		throw e; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.reply != null) { | ||||
|  |  | |||
|  | @ -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); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 }); | ||||
|  |  | |||
|  | @ -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(), | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue