Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
60e0b19372 | |||
922eb937ff | |||
87573284f1 | |||
a91c585f55 | |||
953ea21d5e | |||
ecb00968bc | |||
50ad8adb2d | |||
16878caf09 | |||
5bc30c5493 | |||
85d89cf4c4 | |||
db693f598b | |||
0494c770a1 | |||
c473b62aed | |||
f19ac5320e | |||
612e3aafbc | |||
0e97fec451 | |||
e8c8626ee4 | |||
d89e0f07f8 | |||
e7f81a42ce | |||
ac614148b8 | |||
5eb02b4901 | |||
65631525f6 | |||
969435cfe9 | |||
c932f7a25b | |||
42d164dc57 | |||
a7e60f80bd | |||
3dd5f313b7 | |||
883962c393 | |||
8a30ff1c76 | |||
e47c354916 | |||
496f42805d | |||
c3d34bda37 | |||
bb6ede2b8f | |||
822400a1ba | |||
e3e08843f1 | |||
ce0d4f77fa | |||
94fdb4e974 | |||
4d425fc8a4 | |||
c6cdfa2f5a | |||
0fff2e4f16 | |||
80a2172715 | |||
5a0a297634 | |||
948a133b7b | |||
2ee826c958 | |||
539409faf8 | |||
606e46e4d7 | |||
a179cfd69a | |||
d8379253d4 | |||
c3344fbd68 | |||
4cebd6e84a | |||
90fbf9dbb0 | |||
d365b9f634 |
@ -1,31 +0,0 @@
|
||||
---
|
||||
name: 🐛 Bug Report (🖥️Client specific)
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ⚠️bug?, 🖥️Client
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the bug is -->
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Environment
|
||||
|
||||
<!-- Tell us where on the platform it happens -->
|
||||
<!-- e.g. desktop or mobile version, your browser, your OS -->
|
@ -1,31 +0,0 @@
|
||||
---
|
||||
name: 🐛 Bug Report (⚙️Server specific)
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ⚠️bug?, ⚙️Server
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the bug is -->
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Environment
|
||||
|
||||
<!-- Tell us where on the platform it happens -->
|
||||
<!-- e.g. your Node.js version, your OS -->
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
name: ✨ Feature Request (🖥️Client specific)
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ✨Feature, 🖥️Client
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the suggestion is -->
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
name: ✨ Feature Request (⚙️Server specific)
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ✨Feature, ⚙️Server
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the suggestion is -->
|
24
CHANGELOG.md
24
CHANGELOG.md
@ -1,6 +1,30 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
If you encounter any problems with updating, please try the following:
|
||||
1. `npm run clean` or `npm run cleanall`
|
||||
2. Retry update (Don't forget `npm i`)
|
||||
|
||||
10.93.0
|
||||
----------
|
||||
* フォローリストをインポートできるように
|
||||
* embedプレイヤーを閉じれるように
|
||||
* リストをインポートしたときにプロキシアカウントがフォローするように修正
|
||||
* Web Share Targetの動作を修正
|
||||
* おすすめアンケートのチョイスを修正
|
||||
* デザインの調整
|
||||
|
||||
10.92.4
|
||||
----------
|
||||
* リストのエクスポートをできるように
|
||||
* ジョブキューウィジェットを追加
|
||||
* URLプレビューのサムネイルが表示されないことがある問題を修正
|
||||
|
||||
10.92.3
|
||||
----------
|
||||
* 管理画面の各種ジョブ数がおかしい問題を修正
|
||||
* ジョブキューの動作を調整
|
||||
|
||||
10.92.2
|
||||
----------
|
||||
* 管理画面で各種ジョブ数を一覧できるように
|
||||
|
@ -137,9 +137,9 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
|
||||
</tr></table>
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%3D" alt="natalie" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="@Hekovic@gyutte.site" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="Hekovic" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td>
|
||||
@ -147,14 +147,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
|
||||
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=18072312">@Hekovic@gyutte.site</a></td>
|
||||
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
|
||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
||||
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||
</tr></table>
|
||||
|
||||
**Last updated:** Thu, 07 Mar 2019 11:30:05 UTC
|
||||
**Last updated:** Tue, 12 Mar 2019 00:50:06 UTC
|
||||
<!-- PATREON_END -->
|
||||
|
||||
:four_leaf_clover: Copyright
|
||||
|
@ -118,7 +118,7 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex
|
||||
4. `NODE_ENV=production npm run build`
|
||||
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
|
||||
なにか問題が発生した場合は、`npm run clean`すると直る場合があります。
|
||||
なにか問題が発生した場合は、`npm run clean`または`npm run cleanall`すると直る場合があります。
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
@ -190,6 +190,7 @@ common:
|
||||
remain-deleted-note: "I nadále zobrazovat odstraněné příspěvky"
|
||||
sound: "Zvuk"
|
||||
enable-sounds: "Povolit zvuk"
|
||||
enable-sounds-desc: "Přehrát zvuk, například při odeslání nebo přijetí příspěvku, či zprávy. Toto nastavení je uloženo v prohlížeči."
|
||||
volume: "Hlasitost"
|
||||
test: "Test"
|
||||
update: "Aktualizace Misskey"
|
||||
@ -300,6 +301,7 @@ common/views/pages/explore.vue:
|
||||
recently-registered-users: "Nedávno registrovaní uživatelé"
|
||||
popular-tags: "Populární tagy"
|
||||
federated: "Z fediverse"
|
||||
explore: "Prozkoumat {host}"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Otevřít v přehrávači"
|
||||
common/views/components/user-list.vue:
|
||||
@ -412,12 +414,15 @@ common/views/components/nav.vue:
|
||||
feedback: "Zpětná vazba"
|
||||
common/views/components/note-menu.vue:
|
||||
mention: "Zmínění"
|
||||
detail: "Více"
|
||||
copy-content: "Zkopírovat obsah"
|
||||
copy-link: "Zkopírovat odkaz"
|
||||
favorite: "Přidat do oblíbených"
|
||||
unfavorite: "Odebrat z oblízených"
|
||||
watch: "Sledovat"
|
||||
unwatch: "Přestat sledovat"
|
||||
pin: "Připnout"
|
||||
unpin: "Odepnout"
|
||||
delete: "Odstranit"
|
||||
delete-confirm: "Opravdu chcete smazat tento příspěvek?"
|
||||
remote: "Ukázat originální poznámku"
|
||||
@ -554,10 +559,12 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "Váš e-mail byl ověřen"
|
||||
email-not-verified: "Váš email není potvrzen. Prosím zkontrolujte si svou schránku."
|
||||
export: "Exportovat"
|
||||
import: "Importovat"
|
||||
export-targets:
|
||||
following-list: "Seznam sledujících"
|
||||
mute-list: "Seznam ztlumených uživatelů"
|
||||
blocking-list: "Seznam blokovaných uživatelů"
|
||||
user-lists: "Seznamy"
|
||||
enter-password: "Prosím, zadejte Vaše heslo"
|
||||
danger-zone: "Nebezpečná zóna"
|
||||
delete-account: "Smazat účet"
|
||||
@ -694,6 +701,7 @@ desktop/views/components/note.vue:
|
||||
renote: "Renote"
|
||||
add-reaction: "Přidat reakci"
|
||||
undo-reaction: "Odebrat reakci"
|
||||
detail: "Více"
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
desktop/views/components/notes.vue:
|
||||
@ -1029,8 +1037,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "Vyhledávání je vypnuté pro tuto instanci."
|
||||
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Sdílet na {name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -339,6 +339,9 @@ common/views/components/profile-editor.vue:
|
||||
banner: "Banner"
|
||||
save: "Speichern"
|
||||
export: "Exportieren"
|
||||
import: "Importieren"
|
||||
export-targets:
|
||||
user-lists: "Listen"
|
||||
enter-password: "Bitte Passwort eingeben"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Laden"
|
||||
|
@ -114,7 +114,7 @@ common:
|
||||
a: "What are you doing?"
|
||||
b: "What's happening?"
|
||||
c: "What’s on your mind?"
|
||||
d: "Would you post any words?"
|
||||
d: "What do you want to say?"
|
||||
e: "Write here"
|
||||
f: "Waiting for your writing."
|
||||
settings: "Settings"
|
||||
@ -285,7 +285,7 @@ auth/views/form.vue:
|
||||
account-read: "View account information."
|
||||
account-write: "Modify account information."
|
||||
note-write: "Post."
|
||||
like-write: "React to posts."
|
||||
like-write: "Express yourself about this post."
|
||||
following-write: "Follow and unfollow."
|
||||
drive-read: "Read your drive."
|
||||
drive-write: "Upload/delete files in your drive."
|
||||
@ -304,7 +304,7 @@ auth/views/index.vue:
|
||||
error: "Session does not exist."
|
||||
sign-in: "Please sign in."
|
||||
common/views/pages/explore.vue:
|
||||
verified-users: "Verified accounts"
|
||||
verified-users: "Official accounts"
|
||||
popular-users: "Popular users"
|
||||
recently-updated-users: "Recently active users"
|
||||
recently-registered-users: "Users who joined recently"
|
||||
@ -314,6 +314,7 @@ common/views/pages/explore.vue:
|
||||
users-info: "Currently, {users} users are registered here"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Enable playback"
|
||||
disable-player: "Close the player"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "There are no users."
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
@ -647,12 +648,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "Your email has been verified."
|
||||
email-not-verified: "Email address is not confirmed. Please check your inbox."
|
||||
export: "Export"
|
||||
import: "Import"
|
||||
export-and-import: "Export and Import"
|
||||
export-targets:
|
||||
all-notes: "All posted Notes"
|
||||
following-list: "List of followers"
|
||||
mute-list: "List of muted accounts"
|
||||
blocking-list: "List of blocked accounts"
|
||||
user-lists: "Lists"
|
||||
export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
|
||||
import-requested: "You have initiated an import. This may take quite some time."
|
||||
enter-password: "Please enter your password"
|
||||
danger-zone: "Cautious options"
|
||||
delete-account: "Remove the account"
|
||||
@ -1347,8 +1352,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "Search feature is turned off in the settings for this instance."
|
||||
not-found: "No posts were found for '{q}'"
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Share on {name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "No posts contains \"{q}\" found."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -103,6 +103,32 @@ common:
|
||||
tags: "Etiquetas"
|
||||
blocking: "Bloquear"
|
||||
password: "Contraseña"
|
||||
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
|
||||
line-width: "Grosor de línea"
|
||||
line-width-thick: "Grosor"
|
||||
font-size: "Tamaño del texto"
|
||||
font-size-x-small: "Muy pequeño"
|
||||
font-size-small: "Pequeño"
|
||||
font-size-medium: "Normal"
|
||||
font-size-large: "Grande"
|
||||
font-size-x-large: "Muy grande"
|
||||
deck-column-align: "Alineamiento de las columnas"
|
||||
deck-column-align-center: "Centrar"
|
||||
deck-column-align-left: "Izquierda"
|
||||
deck-column-align-flexible: "Flexible"
|
||||
deck-column-width: "Ancho de las columnas"
|
||||
deck-column-width-narrow: "Estrecho"
|
||||
deck-column-width-narrower: "Un poco estrecho"
|
||||
deck-column-width-normal: "Normal"
|
||||
deck-column-width-wider: "Un poco ancho"
|
||||
deck-column-width-wide: "Ancho"
|
||||
use-shadow: "Usar sombras en la Interfaz de Usuario"
|
||||
rounded-corners: "Esquinas redondeadas en la Interfaz de Usuario"
|
||||
circle-icons: "Usar iconos circulares"
|
||||
contrasted-acct: "Añadir contraste al nombre de usuario"
|
||||
wallpaper: "Fondo de pantalla"
|
||||
choose-wallpaper: "Escoge un fondo de pantalla"
|
||||
navbar-position-left: "Izquierda"
|
||||
search: "Buscar"
|
||||
delete: "eliminar"
|
||||
loading: "cargando"
|
||||
@ -395,9 +421,11 @@ common/views/components/profile-editor.vue:
|
||||
save: "Guardar"
|
||||
email-address: "Correo electrónico"
|
||||
export: "Exportar"
|
||||
import: "Importar"
|
||||
export-targets:
|
||||
mute-list: "Silenciar"
|
||||
blocking-list: "Bloquear"
|
||||
user-lists: "Listas"
|
||||
enter-password: "Escribe una contraseña"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Usuarios"
|
||||
|
@ -522,11 +522,13 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "L’adresse du courrier électronique a été vérifiée."
|
||||
email-not-verified: "Adresse de courriel n’est pas confirmée. Veuillez vérifier votre boite de réception."
|
||||
export: "Exporter"
|
||||
import: "Importer"
|
||||
export-targets:
|
||||
all-notes: "Toutes les notes publiées"
|
||||
following-list: "Liste des abonnements"
|
||||
mute-list: "Liste des comptes mis en sourdine"
|
||||
blocking-list: "Liste des comptes bloqués"
|
||||
user-lists: "Listes"
|
||||
export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
|
||||
enter-password: "Veuillez saisir votre mot de passe"
|
||||
danger-zone: "Zone de danger"
|
||||
@ -1207,8 +1209,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "La fonction de recherche est désactivée dans les paramètres de l’instance."
|
||||
not-found: "Aucune publication trouvée pour « {q} »."
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Partager avec {name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "Aucune publication contenant « {q} » n’a été trouvée."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -334,6 +334,7 @@ common/views/pages/explore.vue:
|
||||
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "プレイヤーを開く"
|
||||
disable-player: "プレイヤーを閉じる"
|
||||
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "ユーザーがいません"
|
||||
@ -701,12 +702,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "メールアドレスが確認されました"
|
||||
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
export-and-import: "エクスポートとインポート"
|
||||
export-targets:
|
||||
all-notes: "すべての投稿データ"
|
||||
following-list: "フォロー"
|
||||
mute-list: "ミュート"
|
||||
blocking-list: "ブロック"
|
||||
user-lists: "リスト"
|
||||
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
|
||||
import-requested: "インポートをリクエストしました。これには時間がかかる場合があります。"
|
||||
enter-password: "パスワードを入力してください"
|
||||
danger-zone: "危険な設定"
|
||||
delete-account: "アカウントを削除"
|
||||
@ -1175,7 +1180,7 @@ admin/views/dashboard.vue:
|
||||
federated: "連合"
|
||||
|
||||
admin/views/queue.vue:
|
||||
operation: "操作"
|
||||
title: "キュー"
|
||||
remove-all-jobs: "すべてのジョブをクリア"
|
||||
|
||||
admin/views/abuse.vue:
|
||||
@ -1486,9 +1491,6 @@ desktop/views/pages/search.vue:
|
||||
not-available: "検索機能はインスタンスの設定で無効になっています。"
|
||||
not-found: "「{q}」に関する投稿は見つかりませんでした。"
|
||||
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "{name}で共有"
|
||||
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
|
||||
|
||||
|
@ -476,10 +476,12 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "このメールアドレスOKや!"
|
||||
email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
export-targets:
|
||||
following-list: "フォロー"
|
||||
mute-list: "ミュート"
|
||||
blocking-list: "ブロック"
|
||||
user-lists: "リスト"
|
||||
enter-password: "パスワードを入れてや"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "ユーザー"
|
||||
|
@ -647,12 +647,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "매일 주소가 확인되었습니다"
|
||||
email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
|
||||
export: "내보내기"
|
||||
import: "가져오기"
|
||||
export-and-import: "내보내기와 가져오기"
|
||||
export-targets:
|
||||
all-notes: "모든 글 데이터"
|
||||
following-list: "팔로잉"
|
||||
mute-list: "뮤트"
|
||||
blocking-list: "차단"
|
||||
user-lists: "리스트"
|
||||
export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
|
||||
import-requested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
|
||||
enter-password: "비밀번호를 입력하여 주십시오"
|
||||
danger-zone: "위험한 설정"
|
||||
delete-account: "계정 삭제"
|
||||
@ -1347,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다."
|
||||
not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "{name}(으)로 공유"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -196,6 +196,7 @@ common/views/components/profile-editor.vue:
|
||||
banner: "Omslagfoto"
|
||||
export-targets:
|
||||
following-list: "Volgend"
|
||||
user-lists: "Lijsten"
|
||||
enter-password: "Voer het wachtwoord in"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Gebruiker"
|
||||
|
@ -187,6 +187,7 @@ common/views/components/profile-editor.vue:
|
||||
save: "Lagre"
|
||||
export-targets:
|
||||
following-list: "Følger"
|
||||
user-lists: "Lister"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Bruker"
|
||||
common/views/widgets/broadcast.vue:
|
||||
|
@ -122,7 +122,23 @@ common:
|
||||
appearance: "Wygląd"
|
||||
behavior: "Zachowanie"
|
||||
note-visibility: "Widoczność wpisów"
|
||||
line-width-thin: "Cienka"
|
||||
line-width-normal: "Normalna"
|
||||
line-width-thick: "Gruba"
|
||||
font-size: "Rozmiar tekstu"
|
||||
font-size-medium: "Normalna"
|
||||
font-size-x-large: "Duży"
|
||||
deck-column-align-center: "Po środku"
|
||||
deck-column-align-left: "Z lewej"
|
||||
deck-column-align-flexible: "Elastyczne"
|
||||
deck-column-width: "Szerokość kolumn w talii"
|
||||
deck-column-width-narrow: "Wąska"
|
||||
deck-column-width-narrower: "Trochę wąska"
|
||||
deck-column-width-normal: "Normalna"
|
||||
deck-column-width-wider: "Trochę szerokie"
|
||||
deck-column-width-wide: "Szeroka"
|
||||
timeline: "Oś czasu"
|
||||
navbar-position-left: "Z lewej"
|
||||
search: "Szukaj"
|
||||
delete: "Usuń"
|
||||
loading: "Ładowanie"
|
||||
@ -474,10 +490,12 @@ common/views/components/profile-editor.vue:
|
||||
email-address: "Adres e-mail"
|
||||
email-verified: "Twój adres e-mail został zweryfikowany."
|
||||
export: "Eksportuj"
|
||||
import: "Importuj"
|
||||
export-targets:
|
||||
following-list: "Śledzeni"
|
||||
mute-list: "Wycisz"
|
||||
blocking-list: "Zablokuj"
|
||||
user-lists: "Listy"
|
||||
enter-password: "Wprowadź hasło"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Użytkownicy"
|
||||
|
@ -647,12 +647,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "电子邮件地址已验证"
|
||||
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
|
||||
export: "导出"
|
||||
import: "导入"
|
||||
export-and-import: "导出/导入"
|
||||
export-targets:
|
||||
all-notes: "所有发帖"
|
||||
following-list: "关注列表"
|
||||
mute-list: "屏蔽列表"
|
||||
blocking-list: "黑名单"
|
||||
user-lists: "列表"
|
||||
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
|
||||
import-requested: "导入请求已提交。这可能需要花一点时间。"
|
||||
enter-password: "请输入您的密码"
|
||||
danger-zone: "危险选项"
|
||||
delete-account: "删除帐户"
|
||||
@ -1347,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "在此实例的设置中关闭搜索功能。"
|
||||
not-found: "没有找到“{q}”的帖子"
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "共享{name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "没有找到带有主题标签“{q}”的帖子"
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.92.2",
|
||||
"version": "10.93.0",
|
||||
"codename": "nighthike",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-card>
|
||||
<template #title>{{ $t('operation') }}</template>
|
||||
<section>
|
||||
<header>Deliver</header>
|
||||
<ui-horizon-group inputs v-if="stats">
|
||||
<ui-input :value="stats.deliver.waiting | number" type="text" readonly>
|
||||
<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
|
||||
<section class="wptihjuy">
|
||||
<header><fa :icon="faPaperPlane"/> Deliver</header>
|
||||
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
|
||||
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
|
||||
<span>Waiting</span>
|
||||
</ui-input>
|
||||
<ui-input :value="stats.deliver.delayed | number" type="text" readonly>
|
||||
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
|
||||
<span>Delayed</span>
|
||||
</ui-input>
|
||||
<ui-input :value="stats.deliver.active | number" type="text" readonly>
|
||||
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<div ref="deliverChart" class="chart"></div>
|
||||
</section>
|
||||
<section>
|
||||
<header>Inbox</header>
|
||||
<ui-horizon-group inputs v-if="stats">
|
||||
<ui-input :value="stats.inbox.waiting | number" type="text" readonly>
|
||||
<section class="wptihjuy">
|
||||
<header><fa :icon="faInbox"/> Inbox</header>
|
||||
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
|
||||
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
|
||||
<span>Waiting</span>
|
||||
</ui-input>
|
||||
<ui-input :value="stats.inbox.delayed | number" type="text" readonly>
|
||||
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
|
||||
<span>Delayed</span>
|
||||
</ui-input>
|
||||
<ui-input :value="stats.inbox.active | number" type="text" readonly>
|
||||
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<div ref="inboxChart" class="chart"></div>
|
||||
</section>
|
||||
<section>
|
||||
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
|
||||
@ -40,29 +42,125 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import ApexCharts from 'apexcharts';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import { faTasks, faInbox } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/queue.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
stats: null
|
||||
stats: [],
|
||||
deliverChart: null,
|
||||
inboxChart: null,
|
||||
faTasks, faPaperPlane, faInbox
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
const fetchStats = () => {
|
||||
this.$root.api('admin/queue/stats', {}, true).then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
computed: {
|
||||
latestStats(): any {
|
||||
return this.stats[this.stats.length - 1];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
stats(stats) {
|
||||
this.inboxChart.updateSeries([{
|
||||
name: 'Active',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
|
||||
}, {
|
||||
name: 'Waiting',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.deliverChart.updateSeries([{
|
||||
name: 'Active',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
|
||||
}, {
|
||||
name: 'Waiting',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const chartOpts = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 200,
|
||||
animations: {
|
||||
dynamicAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
},
|
||||
},
|
||||
series: [] as any,
|
||||
colors: ['#00BCD4', '#FFEB3B', '#e53935'],
|
||||
xaxis: {
|
||||
type: 'numeric',
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
min: 0,
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
|
||||
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
|
||||
|
||||
const clock = setInterval(fetchStats, 1000);
|
||||
this.inboxChart.render();
|
||||
this.deliverChart.render();
|
||||
|
||||
const connection = this.$root.stream.useSharedConnection('queueStats');
|
||||
connection.on('stats', this.onStats);
|
||||
connection.on('statsLog', this.onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 100
|
||||
});
|
||||
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
clearInterval(clock);
|
||||
connection.dispose();
|
||||
this.inboxChart.destroy();
|
||||
this.deliverChart.destroy();
|
||||
});
|
||||
},
|
||||
|
||||
@ -83,6 +181,24 @@ export default Vue.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 100) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.wptihjuy
|
||||
> .chart
|
||||
min-height 200px !important
|
||||
|
||||
</style>
|
||||
|
@ -51,12 +51,12 @@
|
||||
<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-button @click="save(true)">{{ $t('save') }}</ui-button>
|
||||
<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
|
||||
</ui-form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('advanced') }}</header>
|
||||
<header><fa :icon="faCogs"/> {{ $t('advanced') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
|
||||
@ -66,7 +66,7 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('privacy') }}</header>
|
||||
<header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
|
||||
@ -76,7 +76,7 @@
|
||||
</section>
|
||||
|
||||
<section v-if="enableEmail">
|
||||
<header>{{ $t('email') }}</header>
|
||||
<header><fa :icon="faEnvelope"/> {{ $t('email') }}</header>
|
||||
|
||||
<div>
|
||||
<template v-if="$store.state.i.email != null">
|
||||
@ -84,12 +84,12 @@
|
||||
<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
|
||||
</template>
|
||||
<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
|
||||
<ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
|
||||
<ui-button @click="updateEmail()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('export') }}</header>
|
||||
<header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-select v-model="exportTarget">
|
||||
@ -97,8 +97,12 @@
|
||||
<option value="following">{{ $t('export-targets.following-list') }}</option>
|
||||
<option value="mute">{{ $t('export-targets.mute-list') }}</option>
|
||||
<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
|
||||
<option value="user-lists">{{ $t('export-targets.user-lists') }}</option>
|
||||
</ui-select>
|
||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||
<ui-horizon-group class="fit-bottom">
|
||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||
<ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -118,7 +122,8 @@ import { apiUrl, host } from '../../../../config';
|
||||
import { toUnicode } from 'punycode';
|
||||
import langmap from 'langmap';
|
||||
import { unique } from '../../../../../../prelude/array';
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/profile-editor.vue'),
|
||||
@ -147,7 +152,7 @@ export default Vue.extend({
|
||||
avatarUploading: false,
|
||||
bannerUploading: false,
|
||||
exportTarget: 'notes',
|
||||
faDownload
|
||||
faDownload, faUpload, faSave, faEnvelope, faUnlockAlt, faBoxes, faCogs
|
||||
};
|
||||
},
|
||||
|
||||
@ -284,6 +289,7 @@ export default Vue.extend({
|
||||
this.exportTarget == 'following' ? 'i/export-following' :
|
||||
this.exportTarget == 'mute' ? 'i/export-mute' :
|
||||
this.exportTarget == 'blocking' ? 'i/export-blocking' :
|
||||
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
|
||||
null, {});
|
||||
|
||||
this.$root.dialog({
|
||||
@ -292,6 +298,22 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
doImport() {
|
||||
this.$chooseDriveFile().then(file => {
|
||||
this.$root.api(
|
||||
this.exportTarget == 'following' ? 'i/import-following' :
|
||||
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
|
||||
null, {
|
||||
fileId: file.id
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('import-requested')
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
const { canceled: canceled, result: password } = await this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button>
|
||||
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
|
||||
</div>
|
||||
<div v-else-if="tweetUrl && detail" class="twitter">
|
||||
@ -126,6 +127,22 @@ export default Vue.extend({
|
||||
position relative
|
||||
width 100%
|
||||
|
||||
> button
|
||||
position absolute
|
||||
top -1.5em
|
||||
right 0
|
||||
font-size 1em
|
||||
width 1.5em
|
||||
height 1.5em
|
||||
padding 0
|
||||
margin 0
|
||||
color var(--text)
|
||||
background rgba(128, 128, 128, 0.2)
|
||||
opacity 0.7
|
||||
|
||||
&:hover
|
||||
opacity 0.9
|
||||
|
||||
> iframe
|
||||
height 100%
|
||||
left 0
|
||||
|
@ -26,6 +26,7 @@
|
||||
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
</select>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h1>{{ $t('share-with', { name }) }}</h1>
|
||||
<div>
|
||||
<mk-signin v-if="!$store.getters.isSignedIn"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
|
||||
<p v-if="posted" class="posted"><fa icon="check"/></p>
|
||||
</div>
|
||||
<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
|
||||
@ -20,9 +20,21 @@ export default Vue.extend({
|
||||
return {
|
||||
name: null,
|
||||
posted: false,
|
||||
text: new URLSearchParams(location.search).get('text')
|
||||
text: new URLSearchParams(location.search).get('text'),
|
||||
url: new URLSearchParams(location.search).get('url'),
|
||||
title: new URLSearchParams(location.search).get('title'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
template(): string {
|
||||
let t = '';
|
||||
if (this.title && this.url) t += `【[${title}](${url})】\n`;
|
||||
if (this.title && !this.url) t += `【${title}】\n`;
|
||||
if (this.text) t += `${text}\n`;
|
||||
if (!this.title && this.url) t += `${url}`;
|
||||
return t.trim();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
window.close();
|
@ -31,3 +31,4 @@ Vue.component('mkw-version', wVersion);
|
||||
Vue.component('mkw-hashtags', wHashtags);
|
||||
Vue.component('mkw-instance', wInstance);
|
||||
Vue.component('mkw-post-form', wPostForm);
|
||||
Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default));
|
||||
|
157
src/client/app/common/views/widgets/queue.vue
Normal file
157
src/client/app/common/views/widgets/queue.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-container :show-header="!props.compact">
|
||||
<template #header><fa :icon="faTasks"/>Queue</template>
|
||||
|
||||
<div class="mntrproz">
|
||||
<div>
|
||||
<b>In</b>
|
||||
<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
|
||||
<div ref="in"></div>
|
||||
</div>
|
||||
<div>
|
||||
<b>Out</b>
|
||||
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
|
||||
<div ref="out"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../define-widget';
|
||||
import { faTasks } from '@fortawesome/free-solid-svg-icons';
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
export default define({
|
||||
name: 'queue',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
data() {
|
||||
return {
|
||||
stats: [],
|
||||
inChart: null,
|
||||
outChart: null,
|
||||
faTasks
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
stats(stats) {
|
||||
this.inChart.updateSeries([{
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.outChart.updateSeries([{
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
latestStats(): any {
|
||||
return this.stats[this.stats.length - 1];
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const chartOpts = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 70,
|
||||
animations: {
|
||||
dynamicAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 1
|
||||
},
|
||||
series: [{
|
||||
data: [] as any
|
||||
}, {
|
||||
data: [] as any
|
||||
}],
|
||||
yaxis: {
|
||||
min: 0,
|
||||
}
|
||||
};
|
||||
|
||||
this.inChart = new ApexCharts(this.$refs.in, chartOpts);
|
||||
this.outChart = new ApexCharts(this.$refs.out, chartOpts);
|
||||
|
||||
this.inChart.render();
|
||||
this.outChart.render();
|
||||
|
||||
const connection = this.$root.stream.useSharedConnection('queueStats');
|
||||
connection.on('stats', this.onStats);
|
||||
connection.on('statsLog', this.onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 50
|
||||
});
|
||||
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
connection.dispose();
|
||||
this.inChart.destroy();
|
||||
this.outChart.destroy();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
this.save();
|
||||
},
|
||||
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 50) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mntrproz
|
||||
display flex
|
||||
padding 4px
|
||||
|
||||
> div
|
||||
width 50%
|
||||
padding 4px
|
||||
|
||||
> b
|
||||
display block
|
||||
font-size 12px
|
||||
color var(--text)
|
||||
|
||||
> span
|
||||
position absolute
|
||||
top 4px
|
||||
right 4px
|
||||
opacity 0.7
|
||||
font-size 12px
|
||||
color var(--text)
|
||||
|
||||
</style>
|
@ -18,7 +18,7 @@ import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||
import MkDrive from './views/pages/drive.vue';
|
||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||
import MkReversi from './views/pages/games/reversi.vue';
|
||||
import MkShare from './views/pages/share.vue';
|
||||
import MkShare from '../common/views/pages/share.vue';
|
||||
import MkFollow from '../common/views/pages/follow.vue';
|
||||
import MkNotFound from '../common/views/pages/not-found.vue';
|
||||
import MkSettings from './views/pages/settings.vue';
|
||||
|
@ -27,6 +27,7 @@
|
||||
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
</select>
|
||||
|
@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr">
|
||||
<h1>{{ this.$t('share-with', { name }) }}</h1>
|
||||
<div>
|
||||
<mk-signin v-if="!$store.getters.isSignedIn"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
|
||||
<p v-if="posted" class="posted"><fa icon="check"/></p>
|
||||
</div>
|
||||
<button v-if="posted" class="ui button" @click="close">{{ $t('@.close') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/share.vue'),
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
posted: false,
|
||||
text: new URLSearchParams(location.search).get('text')
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.name = meta.name;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.pptjhabgjtt7kwskbfv4y3uml6fpuhmr
|
||||
padding 16px
|
||||
|
||||
> h1
|
||||
margin 0 0 8px 0
|
||||
color #555
|
||||
font-size 20px
|
||||
text-align center
|
||||
|
||||
> div
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
background #fff
|
||||
border solid 1px rgba(#000, 0.1)
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
> .posted
|
||||
display block
|
||||
margin 0
|
||||
padding 64px
|
||||
text-align center
|
||||
|
||||
> button
|
||||
display block
|
||||
margin 16px auto
|
||||
</style>
|
@ -26,7 +26,7 @@ import MkUserLists from './views/pages/user-lists.vue';
|
||||
import MkUserList from './views/pages/user-list.vue';
|
||||
import MkReversi from './views/pages/games/reversi.vue';
|
||||
import MkTag from './views/pages/tag.vue';
|
||||
import MkShare from './views/pages/share.vue';
|
||||
import MkShare from '../common/views/pages/share.vue';
|
||||
import MkFollow from '../common/views/pages/follow.vue';
|
||||
import MkNotFound from '../common/views/pages/not-found.vue';
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="version">{{ $t('@.widgets.version') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="memo">{{ $t('@.widgets.memo') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
|
@ -43,6 +43,11 @@
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"url_template": "share?text=【{title}】%0A{text}%0A{url}"
|
||||
"action": "/share/",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ export type Source = {
|
||||
host: string;
|
||||
port: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
elasticsearch: {
|
||||
host: string;
|
||||
|
61
src/daemons/queue-stats.ts
Normal file
61
src/daemons/queue-stats.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import * as Deque from 'double-ended-queue';
|
||||
import Xev from 'xev';
|
||||
import { deliverQueue, inboxQueue } from '../queue';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 1000;
|
||||
|
||||
/**
|
||||
* Report queue stats regularly
|
||||
*/
|
||||
export default function() {
|
||||
const log = new Deque<any>();
|
||||
|
||||
ev.on('requestQueueStatsLog', x => {
|
||||
ev.emit(`queueStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50));
|
||||
});
|
||||
|
||||
let activeDeliverJobs = 0;
|
||||
let activeInboxJobs = 0;
|
||||
|
||||
deliverQueue.on('global:active', () => {
|
||||
activeDeliverJobs++;
|
||||
});
|
||||
|
||||
inboxQueue.on('global:active', () => {
|
||||
activeInboxJobs++;
|
||||
});
|
||||
|
||||
async function tick() {
|
||||
const deliverJobCounts = await deliverQueue.getJobCounts();
|
||||
const inboxJobCounts = await inboxQueue.getJobCounts();
|
||||
|
||||
const stats = {
|
||||
deliver: {
|
||||
activeSincePrevTick: activeDeliverJobs,
|
||||
active: deliverJobCounts.active,
|
||||
waiting: deliverJobCounts.waiting,
|
||||
delayed: deliverJobCounts.delayed
|
||||
},
|
||||
inbox: {
|
||||
activeSincePrevTick: activeInboxJobs,
|
||||
active: inboxJobCounts.active,
|
||||
waiting: inboxJobCounts.waiting,
|
||||
delayed: inboxJobCounts.delayed
|
||||
}
|
||||
};
|
||||
|
||||
ev.emit('queueStats', stats);
|
||||
|
||||
log.unshift(stats);
|
||||
if (log.length > 200) log.pop();
|
||||
|
||||
activeDeliverJobs = 0;
|
||||
activeInboxJobs = 0;
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
||||
}
|
@ -5,6 +5,8 @@ export default config.redis ? redis.createClient(
|
||||
config.redis.port,
|
||||
config.redis.host,
|
||||
{
|
||||
auth_pass: config.redis.pass
|
||||
auth_pass: config.redis.pass,
|
||||
prefix: config.redis.prefix,
|
||||
db: config.redis.db || 0
|
||||
}
|
||||
) : null;
|
||||
|
@ -16,6 +16,7 @@ import Xev from 'xev';
|
||||
import Logger from './services/logger';
|
||||
import serverStats from './daemons/server-stats';
|
||||
import notesStats from './daemons/notes-stats';
|
||||
import queueStats from './daemons/queue-stats';
|
||||
import loadConfig from './config/load';
|
||||
import { Config } from './config/types';
|
||||
import { lessThan } from './prelude/array';
|
||||
@ -50,6 +51,7 @@ function main() {
|
||||
if (program.daemons) {
|
||||
serverStats();
|
||||
notesStats();
|
||||
queueStats();
|
||||
}
|
||||
}
|
||||
|
||||
|
79
src/misc/download-text-file.ts
Normal file
79
src/misc/download-text-file.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import chalk from 'chalk';
|
||||
import * as request from 'request';
|
||||
import Logger from '../services/logger';
|
||||
import config from '../config';
|
||||
|
||||
const logger = new Logger('download-text-file');
|
||||
|
||||
export async function downloadTextFile(url: string): Promise<string> {
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
// write content at URL to temp file
|
||||
await new Promise((res, rej) => {
|
||||
logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||
|
||||
const writable = fs.createWriteStream(path);
|
||||
|
||||
writable.on('finish', () => {
|
||||
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||
res();
|
||||
});
|
||||
|
||||
writable.on('error', error => {
|
||||
logger.error(`Download failed: ${chalk.cyan(url)}: ${error}`, {
|
||||
url: url,
|
||||
e: error
|
||||
});
|
||||
rej(error);
|
||||
});
|
||||
|
||||
const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
|
||||
|
||||
const req = request({
|
||||
url: requestUrl,
|
||||
proxy: config.proxy,
|
||||
timeout: 10 * 1000,
|
||||
headers: {
|
||||
'User-Agent': config.userAgent
|
||||
}
|
||||
});
|
||||
|
||||
req.pipe(writable);
|
||||
|
||||
req.on('response', response => {
|
||||
if (response.statusCode !== 200) {
|
||||
logger.error(`Got ${response.statusCode} (${url})`);
|
||||
writable.close();
|
||||
rej(response.statusCode);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', error => {
|
||||
logger.error(`Failed to start download: ${chalk.cyan(url)}: ${error}`, {
|
||||
url: url,
|
||||
e: error
|
||||
});
|
||||
writable.close();
|
||||
rej(error);
|
||||
});
|
||||
});
|
||||
|
||||
logger.succ(`Downloaded to: ${path}`);
|
||||
|
||||
const text = await util.promisify(fs.readFile)(path, 'utf8');
|
||||
|
||||
cleanup();
|
||||
|
||||
return text;
|
||||
}
|
@ -6,9 +6,10 @@ import { ILocalUser } from '../models/user';
|
||||
import { program } from '../argv';
|
||||
|
||||
import processDeliver from './processors/deliver';
|
||||
import processInbox from './processors/process-inbox';
|
||||
import processInbox from './processors/inbox';
|
||||
import processDb from './processors/db';
|
||||
import { queueLogger } from './logger';
|
||||
import { IDriveFile } from '../models/drive-file';
|
||||
|
||||
function initializeQueue(name: string) {
|
||||
return new Queue(name, config.redis != null ? {
|
||||
@ -16,8 +17,9 @@ function initializeQueue(name: string) {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
password: config.redis.pass,
|
||||
db: 1
|
||||
}
|
||||
db: config.redis.db || 0,
|
||||
},
|
||||
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue'
|
||||
} : null);
|
||||
}
|
||||
|
||||
@ -25,6 +27,25 @@ export const deliverQueue = initializeQueue('deliver');
|
||||
export const inboxQueue = initializeQueue('inbox');
|
||||
export const dbQueue = initializeQueue('db');
|
||||
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
|
||||
deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`))
|
||||
.on('error', (error) => deliverLogger.error(`error ${error}`))
|
||||
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
|
||||
|
||||
inboxQueue
|
||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`))
|
||||
.on('error', (error) => inboxLogger.error(`error ${error}`))
|
||||
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
|
||||
|
||||
export function deliver(user: ILocalUser, content: any, to: any) {
|
||||
if (content == null) return null;
|
||||
|
||||
@ -35,10 +56,10 @@ export function deliver(user: ILocalUser, content: any, to: any) {
|
||||
};
|
||||
|
||||
return deliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
attempts: 8,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000
|
||||
delay: 60 * 1000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
@ -52,7 +73,7 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
|
||||
};
|
||||
|
||||
return inboxQueue.add(data, {
|
||||
attempts: 4,
|
||||
attempts: 8,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000
|
||||
@ -116,6 +137,35 @@ export function createExportBlockingJob(user: ILocalUser) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportUserListsJob(user: ILocalUser) {
|
||||
return dbQueue.add('exportUserLists', {
|
||||
user: user
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
|
||||
return dbQueue.add('importFollowing', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
|
||||
return dbQueue.add('importUserLists', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (!program.onlyServer) {
|
||||
deliverQueue.process(128, processDeliver);
|
||||
@ -126,12 +176,12 @@ export default function() {
|
||||
|
||||
export function destroy() {
|
||||
deliverQueue.once('cleaned', (jobs, status) => {
|
||||
queueLogger.succ(`[deliver] Cleaned ${jobs.length} ${status} jobs`);
|
||||
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
deliverQueue.clean(0, 'wait');
|
||||
|
||||
inboxQueue.once('cleaned', (jobs, status) => {
|
||||
queueLogger.succ(`[inbox] Cleaned ${jobs.length} ${status} jobs`);
|
||||
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
inboxQueue.clean(0, 'wait');
|
||||
}
|
||||
|
73
src/queue/processors/db/export-user-lists.ts
Normal file
73
src/queue/processors/db/export-user-lists.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import * as Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import addFile from '../../../services/drive/add-file';
|
||||
import User from '../../../models/user';
|
||||
import dateFormat = require('dateformat');
|
||||
import config from '../../../config';
|
||||
import UserList from '../../../models/user-list';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-user-lists');
|
||||
|
||||
export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Exporting user lists of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||
});
|
||||
|
||||
const lists = await UserList.find({
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
for (const list of lists) {
|
||||
const users = await User.find({
|
||||
_id: { $in: list.userIds }
|
||||
}, {
|
||||
fields: {
|
||||
username: true,
|
||||
host: true
|
||||
}
|
||||
});
|
||||
|
||||
for (const u of users) {
|
||||
const acct = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
|
||||
const content = `${list.title},${acct}`;
|
||||
await new Promise((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
|
||||
const driveFile = await addFile(user, path, fileName);
|
||||
|
||||
logger.succ(`Exported to: ${driveFile._id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
55
src/queue/processors/db/import-following.ts
Normal file
55
src/queue/processors/db/import-following.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import User from '../../../models/user';
|
||||
import config from '../../../config';
|
||||
import follow from '../../../services/following/create';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import resolveUser from '../../../remote/resolve-user';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-following');
|
||||
|
||||
export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Importing following of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||
});
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(job.data.fileId.toString())
|
||||
});
|
||||
|
||||
const url = getOriginalUrl(file);
|
||||
|
||||
const csv = await downloadTextFile(url);
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
const { username, host } = parseAcct(line.trim());
|
||||
|
||||
let target = host === config.host ? await User.findOne({
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await User.findOne({
|
||||
host: host,
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await resolveUser(username, host);
|
||||
}
|
||||
|
||||
logger.info(`Follow ${target._id} ...`);
|
||||
|
||||
follow(user, target);
|
||||
}
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
70
src/queue/processors/db/import-user-lists.ts
Normal file
70
src/queue/processors/db/import-user-lists.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import User from '../../../models/user';
|
||||
import config from '../../../config';
|
||||
import UserList from '../../../models/user-list';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import resolveUser from '../../../remote/resolve-user';
|
||||
import { pushUserToUserList } from '../../../services/user-list/push';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-user-lists');
|
||||
|
||||
export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Importing user lists of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||
});
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(job.data.fileId.toString())
|
||||
});
|
||||
|
||||
const url = getOriginalUrl(file);
|
||||
|
||||
const csv = await downloadTextFile(url);
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
const listName = line.split(',')[0].trim();
|
||||
const { username, host } = parseAcct(line.split(',')[1].trim());
|
||||
|
||||
let list = await UserList.findOne({
|
||||
userId: user._id,
|
||||
title: listName
|
||||
});
|
||||
|
||||
if (list == null) {
|
||||
list = await UserList.insert({
|
||||
createdAt: new Date(),
|
||||
userId: user._id,
|
||||
title: listName,
|
||||
userIds: []
|
||||
});
|
||||
}
|
||||
|
||||
let target = host === config.host ? await User.findOne({
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await User.findOne({
|
||||
host: host,
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
if (list.userIds.some(id => id.equals(target._id))) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await resolveUser(username, host);
|
||||
}
|
||||
|
||||
pushUserToUserList(target, list);
|
||||
}
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
@ -5,6 +5,9 @@ import { exportNotes } from './export-notes';
|
||||
import { exportFollowing } from './export-following';
|
||||
import { exportMute } from './export-mute';
|
||||
import { exportBlocking } from './export-blocking';
|
||||
import { exportUserLists } from './export-user-lists';
|
||||
import { importFollowing } from './import-following';
|
||||
import { importUserLists } from './import-user-lists';
|
||||
|
||||
const jobs = {
|
||||
deleteNotes,
|
||||
@ -13,6 +16,9 @@ const jobs = {
|
||||
exportFollowing,
|
||||
exportMute,
|
||||
exportBlocking,
|
||||
exportUserLists,
|
||||
importFollowing,
|
||||
importUserLists
|
||||
} as any;
|
||||
|
||||
export default function(dbQueue: Bull.Queue) {
|
||||
|
@ -1,18 +1,21 @@
|
||||
import * as Bull from 'bull';
|
||||
import request from '../../remote/activitypub/request';
|
||||
import { queueLogger } from '../logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
import Logger from '../../services/logger';
|
||||
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
let latest: string = null;
|
||||
|
||||
export default async (job: Bull.Job): Promise<void> => {
|
||||
export default async (job: Bull.Job) => {
|
||||
const { host } = new URL(job.data.to);
|
||||
|
||||
try {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2)))
|
||||
queueLogger.debug(`delivering ${latest}`);
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
logger.debug(`delivering ${latest}`);
|
||||
}
|
||||
|
||||
await request(job.data.user, job.data.to, job.data.content);
|
||||
|
||||
@ -29,6 +32,8 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
|
||||
instanceChart.requestSent(i.host, true);
|
||||
});
|
||||
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
// Update stats
|
||||
registerOrFetchInstanceDoc(host).then(i => {
|
||||
@ -44,17 +49,21 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
});
|
||||
|
||||
if (res != null && res.hasOwnProperty('statusCode')) {
|
||||
queueLogger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
|
||||
logger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
|
||||
|
||||
// 4xx
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||
return;
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
return res.statusMessage;
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
queueLogger.warn(`deliver failed: ${res} to=${job.data.to}`);
|
||||
// DNS error, socket error, timeout ...
|
||||
logger.warn(`deliver failed: ${res} to=${job.data.to}`);
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const instance = await Instance.findOne({ host: host.toLowerCase() });
|
||||
if (instance && instance.isBlocked) {
|
||||
logger.warn(`Blocked request: ${host}`);
|
||||
logger.info(`Blocked request: ${host}`);
|
||||
return;
|
||||
}
|
||||
|
@ -29,7 +29,19 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
||||
return;
|
||||
}
|
||||
|
||||
const renote = await resolveNote(note);
|
||||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await resolveNote(note);
|
||||
} catch (e) {
|
||||
// 対象が4xxならスキップ
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`);
|
||||
return;
|
||||
}
|
||||
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
|
@ -27,7 +27,17 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
|
||||
const instance = await fetchMeta();
|
||||
const cache = instance.cacheRemoteFiles;
|
||||
|
||||
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
|
||||
let file;
|
||||
try {
|
||||
file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
|
||||
} catch (e) {
|
||||
// 4xxの場合は添付されてなかったことにする
|
||||
if (e >= 400 && e < 500) {
|
||||
logger.warn(`Ignored image: ${image.url} - ${e}`);
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (file.metadata.isRemote) {
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
|
@ -111,11 +111,22 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
|
||||
const files = note.attachment
|
||||
.map(attach => attach.sensitive = note.sensitive)
|
||||
? await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))
|
||||
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
|
||||
.filter(image => image != null)
|
||||
: [];
|
||||
|
||||
// リプライ
|
||||
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
||||
const reply = note.inReplyTo
|
||||
? await resolveNote(note.inReplyTo, resolver).catch(e => {
|
||||
// 4xxの場合はリプライしてないことにする
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `);
|
||||
return null;
|
||||
}
|
||||
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
|
||||
throw e;
|
||||
})
|
||||
: null;
|
||||
|
||||
// 引用
|
||||
let quote: INote;
|
||||
|
@ -1,9 +1,6 @@
|
||||
import * as request from 'request-promise-native';
|
||||
import { IObject } from './type';
|
||||
import config from '../../config';
|
||||
import { apLogger } from './logger';
|
||||
|
||||
export const logger = apLogger.createSubLogger('resolver');
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
@ -34,7 +31,6 @@ export default class Resolver {
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error(`unknown collection type: ${collection.type}`);
|
||||
throw new Error(`unknown collection type: ${collection.type}`);
|
||||
}
|
||||
}
|
||||
@ -44,7 +40,6 @@ export default class Resolver {
|
||||
|
||||
public async resolve(value: any): Promise<IObject> {
|
||||
if (value == null) {
|
||||
logger.error('resolvee is null (or undefined)');
|
||||
throw new Error('resolvee is null (or undefined)');
|
||||
}
|
||||
|
||||
@ -53,7 +48,6 @@ export default class Resolver {
|
||||
}
|
||||
|
||||
if (this.history.has(value)) {
|
||||
logger.error(`cannot resolve already resolved one: ${value}`);
|
||||
throw new Error('cannot resolve already resolved one');
|
||||
}
|
||||
|
||||
@ -68,12 +62,6 @@ export default class Resolver {
|
||||
Accept: 'application/activity+json, application/ld+json'
|
||||
},
|
||||
json: true
|
||||
}).catch(e => {
|
||||
logger.error(`request error: ${value}: ${e.message}`, {
|
||||
url: value,
|
||||
e: e
|
||||
});
|
||||
throw new Error(`request error: ${e.message}`);
|
||||
});
|
||||
|
||||
if (object === null || (
|
||||
@ -81,10 +69,6 @@ export default class Resolver {
|
||||
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
|
||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||
)) {
|
||||
logger.error(`invalid response: ${value}`, {
|
||||
url: value,
|
||||
object: object
|
||||
});
|
||||
throw new Error('invalid response');
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import define from '../../../define';
|
||||
import { deliverQueue } from '../../../../../queue';
|
||||
import { deliverQueue, inboxQueue } from '../../../../../queue';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@ -12,7 +12,7 @@ export const meta = {
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const deliverJobCounts = await deliverQueue.getJobCounts();
|
||||
const inboxJobCounts = await deliverQueue.getJobCounts();
|
||||
const inboxJobCounts = await inboxQueue.getJobCounts();
|
||||
|
||||
return {
|
||||
deliver: deliverJobCounts,
|
||||
|
18
src/server/api/endpoints/i/export-user-lists.ts
Normal file
18
src/server/api/endpoints/i/export-user-lists.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import define from '../../define';
|
||||
import { createExportUserListsJob } from '../../../../queue';
|
||||
import ms = require('ms');
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1min'),
|
||||
max: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
createExportUserListsJob(user);
|
||||
|
||||
return;
|
||||
});
|
64
src/server/api/endpoints/i/import-following.ts
Normal file
64
src/server/api/endpoints/i/import-following.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import $ from 'cafy';
|
||||
import ID, { transform } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { createImportFollowingJob } from '../../../../queue';
|
||||
import ms = require('ms');
|
||||
import DriveFile from '../../../../models/drive-file';
|
||||
import { ApiError } from '../../error';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 1,
|
||||
},
|
||||
|
||||
params: {
|
||||
fileId: {
|
||||
validator: $.type(ID),
|
||||
transform: transform,
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'b98644cf-a5ac-4277-a502-0b8054a709a3'
|
||||
},
|
||||
|
||||
unexpectedFileType: {
|
||||
message: 'We need csv file.',
|
||||
code: 'UNEXPECTED_FILE_TYPE',
|
||||
id: '660f3599-bce0-4f95-9dde-311fd841c183'
|
||||
},
|
||||
|
||||
tooBigFile: {
|
||||
message: 'That file is too big.',
|
||||
code: 'TOO_BIG_FILE',
|
||||
id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60'
|
||||
},
|
||||
|
||||
emptyFile: {
|
||||
message: 'That file is empty.',
|
||||
code: 'EMPTY_FILE',
|
||||
id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const file = await DriveFile.findOne({
|
||||
_id: ps.fileId
|
||||
});
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||
if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile);
|
||||
if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
|
||||
|
||||
createImportFollowingJob(user, file._id);
|
||||
|
||||
return;
|
||||
});
|
64
src/server/api/endpoints/i/import-user-lists.ts
Normal file
64
src/server/api/endpoints/i/import-user-lists.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import $ from 'cafy';
|
||||
import ID, { transform } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { createImportUserListsJob } from '../../../../queue';
|
||||
import ms = require('ms');
|
||||
import DriveFile from '../../../../models/drive-file';
|
||||
import { ApiError } from '../../error';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 1,
|
||||
},
|
||||
|
||||
params: {
|
||||
fileId: {
|
||||
validator: $.type(ID),
|
||||
transform: transform,
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049'
|
||||
},
|
||||
|
||||
unexpectedFileType: {
|
||||
message: 'We need csv file.',
|
||||
code: 'UNEXPECTED_FILE_TYPE',
|
||||
id: 'a3c9edda-dd9b-4596-be6a-150ef813745c'
|
||||
},
|
||||
|
||||
tooBigFile: {
|
||||
message: 'That file is too big.',
|
||||
code: 'TOO_BIG_FILE',
|
||||
id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9'
|
||||
},
|
||||
|
||||
emptyFile: {
|
||||
message: 'That file is empty.',
|
||||
code: 'EMPTY_FILE',
|
||||
id: '99efe367-ce6e-4d44-93f8-5fae7b040356'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const file = await DriveFile.findOne({
|
||||
_id: ps.fileId
|
||||
});
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||
if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile);
|
||||
if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
|
||||
|
||||
createImportUserListsJob(user, file._id);
|
||||
|
||||
return;
|
||||
});
|
@ -52,10 +52,18 @@ export default define(meta, async (ps, user) => {
|
||||
$ne: user._id,
|
||||
$nin: hideUserIds
|
||||
},
|
||||
visibility: 'public',
|
||||
poll: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
},
|
||||
$or: [{
|
||||
'poll.expiresAt': null
|
||||
}, {
|
||||
'poll.expiresAt': {
|
||||
$gt: new Date()
|
||||
}
|
||||
}],
|
||||
}, {
|
||||
limit: ps.limit,
|
||||
skip: ps.offset,
|
||||
|
@ -1,14 +1,10 @@
|
||||
import $ from 'cafy';
|
||||
import ID, { transform } from '../../../../../misc/cafy-id';
|
||||
import UserList from '../../../../../models/user-list';
|
||||
import { pack as packUser, isRemoteUser, fetchProxyAccount } from '../../../../../models/user';
|
||||
import { publishUserListStream } from '../../../../../services/stream';
|
||||
import { renderActivity } from '../../../../../remote/activitypub/renderer';
|
||||
import renderFollow from '../../../../../remote/activitypub/renderer/follow';
|
||||
import { deliver } from '../../../../../queue';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { pushUserToUserList } from '../../../../../services/user-list/push';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -81,18 +77,5 @@ export default define(meta, async (ps, me) => {
|
||||
}
|
||||
|
||||
// Push the user
|
||||
await UserList.update({ _id: userList._id }, {
|
||||
$push: {
|
||||
userIds: user._id
|
||||
}
|
||||
});
|
||||
|
||||
publishUserListStream(userList._id, 'userAdded', await packUser(user));
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (isRemoteUser(user)) {
|
||||
const proxy = await fetchProxyAccount();
|
||||
const content = renderActivity(renderFollow(proxy, user));
|
||||
deliver(proxy, content, user.inbox);
|
||||
}
|
||||
pushUserToUserList(user, userList);
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import hybridTimeline from './hybrid-timeline';
|
||||
import globalTimeline from './global-timeline';
|
||||
import notesStats from './notes-stats';
|
||||
import serverStats from './server-stats';
|
||||
import queueStats from './queue-stats';
|
||||
import userList from './user-list';
|
||||
import messaging from './messaging';
|
||||
import messagingIndex from './messaging-index';
|
||||
@ -23,6 +24,7 @@ export default {
|
||||
globalTimeline,
|
||||
notesStats,
|
||||
serverStats,
|
||||
queueStats,
|
||||
userList,
|
||||
messaging,
|
||||
messagingIndex,
|
||||
|
41
src/server/api/stream/channels/queue-stats.ts
Normal file
41
src/server/api/stream/channels/queue-stats.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import Xev from 'xev';
|
||||
import Channel from '../channel';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'queueStats';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('queueStats', this.onStats);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onStats(stats: any) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
ev.once(`queueStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
ev.emit('requestQueueStatsLog', {
|
||||
id: body.id,
|
||||
length: body.length
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
ev.removeListener('queueStats', this.onStats);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import * as request from 'request';
|
||||
import fileType from 'file-type';
|
||||
import { serverLogger } from '..';
|
||||
import config from '../../config';
|
||||
import { IImage, ConvertToPng } from '../../services/drive/image-processor';
|
||||
import { IImage, ConvertToPng, ConvertToJpeg } from '../../services/drive/image-processor';
|
||||
import checkSvg from '../../misc/check-svg';
|
||||
|
||||
export async function proxyMedia(ctx: Koa.BaseContext) {
|
||||
@ -29,6 +29,8 @@ export async function proxyMedia(ctx: Koa.BaseContext) {
|
||||
|
||||
if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) {
|
||||
image = await ConvertToPng(path, 498, 280);
|
||||
} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) {
|
||||
image = await ConvertToJpeg(path, 200, 200);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.readFileSync(path),
|
||||
|
@ -3,6 +3,8 @@ import * as request from 'request-promise-native';
|
||||
import summaly from 'summaly';
|
||||
import fetchMeta from '../../misc/fetch-meta';
|
||||
import Logger from '../../services/logger';
|
||||
import config from '../../config';
|
||||
import { query } from '../../prelude/url';
|
||||
|
||||
const logger = new Logger('url-preview');
|
||||
|
||||
@ -44,7 +46,10 @@ module.exports = async (ctx: Koa.BaseContext) => {
|
||||
function wrap(url: string): string {
|
||||
return url != null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `https://images.weserv.nl/?url=${encodeURIComponent(url.replace(/^http:\/\//, '').replace(/^https:\/\//, 'ssl:'))}&w=200&h=200`
|
||||
? `${config.url}/proxy/preview.jpg?${query({
|
||||
url,
|
||||
preview: '1'
|
||||
})}`
|
||||
: url
|
||||
: null;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ html
|
||||
| Misskey
|
||||
|
||||
block desc
|
||||
meta(name='description' content='A planet of fediverse')
|
||||
meta(name='description' content='✨🌎✨ A federated blogging platform ✨🚀✨')
|
||||
|
||||
block meta
|
||||
|
||||
|
@ -42,7 +42,7 @@ export default async (
|
||||
const writable = fs.createWriteStream(path);
|
||||
|
||||
writable.on('finish', () => {
|
||||
logger.succ(`Download succeeded: ${chalk.cyan(url)}`);
|
||||
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||
res();
|
||||
});
|
||||
|
||||
|
23
src/services/user-list/push.ts
Normal file
23
src/services/user-list/push.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user';
|
||||
import UserList, { IUserList } from '../../models/user-list';
|
||||
import { renderActivity } from '../../remote/activitypub/renderer';
|
||||
import { deliver } from '../../queue';
|
||||
import renderFollow from '../../remote/activitypub/renderer/follow';
|
||||
import { publishUserListStream } from '../stream';
|
||||
|
||||
export async function pushUserToUserList(target: IUser, list: IUserList) {
|
||||
await UserList.update({ _id: list._id }, {
|
||||
$push: {
|
||||
userIds: target._id
|
||||
}
|
||||
});
|
||||
|
||||
publishUserListStream(list._id, 'userAdded', await packUser(target));
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (isRemoteUser(target)) {
|
||||
const proxy = await fetchProxyAccount();
|
||||
const content = renderActivity(renderFollow(proxy, target));
|
||||
deliver(proxy, content, target.inbox);
|
||||
}
|
||||
}
|
@ -132,7 +132,7 @@ module.exports = {
|
||||
new WebpackOnBuildPlugin((stats: any) => {
|
||||
fs.writeFileSync('./built/client/meta.json', JSON.stringify({ version: meta.version }), 'utf-8');
|
||||
|
||||
fs.mkdirSync('./built/client/assets/locales', { recursive: true })
|
||||
fs.mkdirSync('./built/client/assets/locales', { recursive: true });
|
||||
|
||||
for (const [lang, locale] of Object.entries(locales))
|
||||
fs.writeFileSync(`./built/client/assets/locales/${lang}.json`, JSON.stringify(locale), 'utf-8');
|
||||
|
Reference in New Issue
Block a user