Compare commits

...

15 Commits

Author SHA1 Message Date
5ce412aeda Merge branch 'develop' 2019-05-16 16:14:00 +09:00
fe6d88e410 11.15.0 2019-05-16 16:13:46 +09:00
a21357248f Dockerを使用している場合、アプデの際にマイグレを自動実行するように 2019-05-16 16:12:30 +09:00
70d710c9a9 管理画面でreCAPTCHAのプレビューを表示するように 2019-05-16 16:08:50 +09:00
183c82fb8d New Crowdin translations (#4924)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)
2019-05-16 13:11:56 +09:00
62cbb92154 Fix #4930 2019-05-16 12:59:09 +09:00
7d70126072 Merge branch 'develop' 2019-05-16 01:18:06 +09:00
54bfffa7b9 11.14.0 2019-05-16 01:16:41 +09:00
3f5b96bf62 Resolve #4928 2019-05-16 01:07:32 +09:00
3d8bbedf1b GIFのサムネイルが生成されないのを修正
#4728
2019-05-15 21:27:20 +09:00
23c9f6a6ca Resolve #4833 2019-05-15 20:41:01 +09:00
5ba8d4949d インスタンスの設定画面を整理 2019-05-15 20:29:47 +09:00
a6befdd541 Fix bug 2019-05-15 17:05:41 +09:00
e5409db0e8 Resolve #4925 2019-05-14 23:54:39 +09:00
678d610cd6 Update CHANGELOG.md 2019-05-14 21:27:20 +09:00
33 changed files with 719 additions and 292 deletions

View File

@ -6,8 +6,6 @@ mongodb:
db: misskey db: misskey
user: syuilo user: syuilo
pass: '' pass: ''
drive:
storage: 'db'
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379

View File

@ -6,8 +6,6 @@ mongodb:
db: test-misskey db: test-misskey
user: admin user: admin
pass: '' pass: ''
drive:
storage: 'db'
# __REDIS__ # __REDIS__
redis: redis:
host: localhost host: localhost

View File

@ -78,61 +78,6 @@ redis:
# port: 9200 # port: 9200
# pass: null # pass: null
# ┌────────────────────────────────────┐
#───┘ File storage (Drive) configuration └──────────────────────
drive:
storage: 'fs'
# OR
#drive:
# storage: 'minio'
# bucket:
# prefix:
# config:
# endPoint:
# port:
# useSSL:
# accessKey:
# secretKey:
# S3/GCS example
#
# * Replace <endpoint> to
# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
# GCS: use 'storage.googleapis.com'
#
# * Replace <region> to
# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
# GCS: not needed (just delete the region line)
#
#drive:
# storage: 'minio'
# bucket: bucket-name
# prefix: files
# baseUrl: https://bucket-name.<endpoint>
# config:
# endPoint: <endpoint>
# region: <region>
# useSSL: true
# accessKey: XXX
# secretKey: YYY
# S3/GCS example (with CDN, custom domain)
#
#drive:
# storage: 'minio'
# bucket: drive.example.com
# prefix: files
# baseUrl: https://drive.example.com
# config:
# endPoint: <endpoint>
# region: <region>
# useSSL: true
# accessKey: XXX
# secretKey: YYY
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View File

@ -8,32 +8,13 @@ If you encounter any problems with updating, please try the following:
Migration Migration
------------------------------ ------------------------------
#### 1 #### 1
`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします:
``` json
{
"type": "postgres",
"host": "PostgreSQLのホスト",
"port": 5432,
"username": "PostgreSQLのユーザー名",
"password": "PostgreSQLのパスワード",
"database": "PostgreSQLのデータベース名",
"entities": ["src/models/entities/*.ts"],
"migrations": ["migration/*.ts"],
"cli": {
"migrationsDir": "migration"
}
}
```
上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。
#### 2
``` ```
npm i -g ts-node npm i -g ts-node
``` ```
#### 3 #### 2
``` ```
ts-node ./node_modules/typeorm/cli.js migration:run npm run migrate
``` ```
How to migrate to v11 from v10 How to migrate to v11 from v10
@ -73,6 +54,28 @@ mongodb:
8. master ブランチに戻す 8. master ブランチに戻す
9. enjoy 9. enjoy
11.15.0 (2019/05/16)
--------------------
### ✨Improvements
* 管理画面でreCAPTCHAのプレビューを表示するように
### 🐛Fixes
* オブジェクトストレージのリージョンの設定が反映されない問題を修正
11.14.0 (2019/05/16)
--------------------
### 注意
このバージョンからオブジェクトストレージの設定は設定ファイルではなく管理画面から行うようになりました。
オブジェクトストレージを使用している場合、アップデートした後管理画面にアクセスしオブジェクトストレージの設定を再度行ってください。
### ✨Improvements
* 特定のユーザーのファイルをすべて削除できるように
* インスタンスの設定画面を整理
### 🐛Fixes
* GIF画像のサムネイルが生成されないのを修正
* 管理画面の「ログ」で複数の除外条件を設定できない問題を修正
11.13.0 (2019/05/14) 11.13.0 (2019/05/14)
-------------------- --------------------
### 注意 ### 注意
@ -85,12 +88,13 @@ mongodb:
* ユーザーや外部インスタンスが生成するリンクにnofollowを追加 * ユーザーや外部インスタンスが生成するリンクにnofollowを追加
* リモートのユーザーページやートページにnoindexを追加 * リモートのユーザーページやートページにnoindexを追加
* 自分のユーザーメニューにはミュートなどを表示しないように * 自分のユーザーメニューにはミュートなどを表示しないように
* デザインの調整
### 🐛Fixes ### 🐛Fixes
* インスタンスブロックを設定できない問題を修正 * インスタンスブロックを設定できない問題を修正
* ピン留め投稿の表示順がおかしい問題を修正 * ピン留め投稿の表示順がおかしい問題を修正
* 設定の「アップデートを確認」でメッセージが正しく表示されない問題を修正 * 設定の「アップデートを確認」でメッセージが正しく表示されない問題を修正
* FFirefoxで自分のメニューが開けない問題を修正 * Firefoxで自分のメニューが開けない問題を修正
* Welcomeページのタグクラウドが動かない問題を修正 * Welcomeページのタグクラウドが動かない問題を修正
11.12.0 (2019/05/10) 11.12.0 (2019/05/10)

View File

@ -199,7 +199,7 @@ const user = await Users.findOne(userId).then(ensure);
``` ```
### Migration作成方法 ### Migration作成方法
コードの変更をした後、`ormconfig.json`書き方はCONTRIBUTING.mdを参照)を用意し、 コードの変更をした後、`ormconfig.json``npm run ormconfig`で生成)を用意し、
``` ```
npm i -g ts-node npm i -g ts-node

View File

@ -39,4 +39,4 @@ COPY --from=builder /misskey/node_modules ./node_modules
COPY --from=builder /misskey/built ./built COPY --from=builder /misskey/built ./built
COPY . ./ COPY . ./
CMD ["npm", "start"] CMD ["npm", "migrateandstart"]

View File

@ -873,7 +873,6 @@ admin/views/index.vue:
users: "Uživatelé" users: "Uživatelé"
federation: "Z fedivesmíru" federation: "Z fedivesmíru"
announcements: "Oznámení" announcements: "Oznámení"
hashtags: "Hashtagy"
queue: "Fronta úloh" queue: "Fronta úloh"
logs: "Logy" logs: "Logy"
back-to-misskey: "Zpět na Misskey" back-to-misskey: "Zpět na Misskey"
@ -898,6 +897,7 @@ admin/views/instance.vue:
maintainer-config: "Informace o administrátorovi" maintainer-config: "Informace o administrátorovi"
maintainer-name: "Jméno administrátora" maintainer-name: "Jméno administrátora"
maintainer-email: "Kontakt na administrátora" maintainer-email: "Kontakt na administrátora"
object-storage-endpoint: "Endpoint"
mb: "V megabajtech" mb: "V megabajtech"
recaptcha-config: "nastavení služby reCAPTCHA" recaptcha-config: "nastavení služby reCAPTCHA"
recaptcha-info: "reCAPTCHA token je povinný. Můžete jej získat na https://www.google.com/recaptcha/intro/" recaptcha-info: "reCAPTCHA token je povinný. Můžete jej získat na https://www.google.com/recaptcha/intro/"

View File

@ -1063,7 +1063,6 @@ admin/views/index.vue:
users: "Users" users: "Users"
federation: "Federation" federation: "Federation"
announcements: "Announcements" announcements: "Announcements"
hashtags: "Hashtags"
abuse: "Abuse" abuse: "Abuse"
queue: "Job Queue" queue: "Job Queue"
logs: "Logs" logs: "Logs"
@ -1098,6 +1097,7 @@ admin/views/instance.vue:
maintainer-name: "Administrator name" maintainer-name: "Administrator name"
maintainer-email: "Contact Administrator" maintainer-email: "Contact Administrator"
drive-config: "Drive settings" drive-config: "Drive settings"
object-storage-endpoint: "Endpoint"
cache-remote-files: "Cache remote files" cache-remote-files: "Cache remote files"
cache-remote-files-desc: "Without this parameter, all remote files are linked to their host server directly. This will be an effective solution to save your server storage, however make remote files invisible to users who set direct-link disabled, since no thumbnail will be generated, increase traffic. It is recommended that this parameter set enabled." cache-remote-files-desc: "Without this parameter, all remote files are linked to their host server directly. This will be an effective solution to save your server storage, however make remote files invisible to users who set direct-link disabled, since no thumbnail will be generated, increase traffic. It is recommended that this parameter set enabled."
local-drive-capacity-mb: "Volume of Drive per user" local-drive-capacity-mb: "Volume of Drive per user"

View File

@ -910,7 +910,6 @@ admin/views/index.vue:
moderators: "Moderadores" moderators: "Moderadores"
users: "Usuarios" users: "Usuarios"
federation: "Federado" federation: "Federado"
hashtags: "Hashtags"
queue: "Cola de trabajos" queue: "Cola de trabajos"
logs: "Registros" logs: "Registros"
back-to-misskey: "Volver a Misskey" back-to-misskey: "Volver a Misskey"

View File

@ -68,7 +68,7 @@ common:
explore: "Découvrir" explore: "Découvrir"
following: "Suit" following: "Suit"
followers: "Abonné·e·s" followers: "Abonné·e·s"
favorites: "Mettre cette note en favoris" favorites: "Favorites"
permissions: permissions:
"read:account": "Afficher les informations du compte" "read:account": "Afficher les informations du compte"
"write:account": "Mettre à jour les informations de votre compte" "write:account": "Mettre à jour les informations de votre compte"
@ -1046,7 +1046,6 @@ admin/views/index.vue:
users: "Utilisateurs" users: "Utilisateurs"
federation: "Fédération" federation: "Fédération"
announcements: "Annonces" announcements: "Annonces"
hashtags: "Hashtags"
abuse: "Abus" abuse: "Abus"
queue: "File dattente" queue: "File dattente"
logs: "Journaux" logs: "Journaux"
@ -1073,14 +1072,19 @@ admin/views/instance.vue:
instance-name: "Nom de linstance" instance-name: "Nom de linstance"
instance-description: "Description de linstance" instance-description: "Description de linstance"
host: "Hôte" host: "Hôte"
icon-url: "URL de l'icône"
logo-url: "URL do logo"
banner-url: "URL de limage de la bannière" banner-url: "URL de limage de la bannière"
error-image-url: "URL de limage derreur" error-image-url: "URL de limage derreur"
languages: "Langue de linstance" languages: "Langue de linstance"
languages-desc: "Vous pouvez en définir plus dune, séparées par des espaces." languages-desc: "Vous pouvez en définir plus dune, séparées par des espaces."
tos-url: "URL des conditions d'utilisation"
repository-url: "URL du dépôt"
maintainer-config: "Informations de ladministrateur" maintainer-config: "Informations de ladministrateur"
maintainer-name: "Nom de ladministrateur" maintainer-name: "Nom de ladministrateur"
maintainer-email: "Contact administratif" maintainer-email: "Contact administratif"
drive-config: "Paramètres du lecteur" drive-config: "Paramètres du lecteur"
object-storage-endpoint: "Point de terminaison"
cache-remote-files: "Mettre en cache des fichiers distants" cache-remote-files: "Mettre en cache des fichiers distants"
local-drive-capacity-mb: "Volume du lecteur par utilisateur" local-drive-capacity-mb: "Volume du lecteur par utilisateur"
remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant" remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant"
@ -1474,8 +1478,11 @@ mobile/views/components/ui.nav.vue:
mobile/views/pages/drive.vue: mobile/views/pages/drive.vue:
contextmenu: contextmenu:
upload: "Téléverser un fichier" upload: "Téléverser un fichier"
url-upload: "Transférer un fichier depuis une URL"
create-folder: "Créer un dossier" create-folder: "Créer un dossier"
rename-folder: "Renommer le dossier" rename-folder: "Renommer le dossier"
move-folder: "Déplacer ce dossier"
delete-folder: "Supprimer ce dossier"
mobile/views/pages/user-lists.vue: mobile/views/pages/user-lists.vue:
title: "Listes" title: "Listes"
mobile/views/pages/signup.vue: mobile/views/pages/signup.vue:
@ -1584,6 +1591,7 @@ dev/views/apps.vue:
create-app: "Créer une app" create-app: "Créer une app"
app-missing: "Aucune application" app-missing: "Aucune application"
dev/views/new-app.vue: dev/views/new-app.vue:
new-app: "Nouvelle application"
create-app: "Création dune application" create-app: "Création dune application"
app-name: "Nom de lapplication" app-name: "Nom de lapplication"
app-name-desc: "Le nom de votre application" app-name-desc: "Le nom de votre application"
@ -1616,7 +1624,9 @@ pages:
enter-variable-name: "Veuillez choisir un nom de variable" enter-variable-name: "Veuillez choisir un nom de variable"
the-variable-name-is-already-used: "Cette variable est déjà utilisée" the-variable-name-is-already-used: "Cette variable est déjà utilisée"
content-blocks: "Contenu du cadre" content-blocks: "Contenu du cadre"
input-blocks: "Entrée"
special-blocks: "Spécial" special-blocks: "Spécial"
post-from-post-form: "Publier ce contenu"
posted-from-post-form: "Publié !" posted-from-post-form: "Publié !"
blocks: blocks:
text: "Texte" text: "Texte"
@ -1653,11 +1663,15 @@ pages:
_counter: _counter:
name: "Nom de la variable" name: "Nom de la variable"
text: "Titre" text: "Titre"
inc: "Augmenter le chiffre"
_button: _button:
text: "Titre" text: "Titre"
action: "L'opération lorsque le bouton sera pressé"
_action: _action:
dialog: "Afficher une fenêtre de dialogue"
_dialog: _dialog:
content: "Contenu" content: "Contenu"
resetRandom: "Réinitialiser le nombre aléatoire"
script: script:
categories: categories:
flow: "Contrôle" flow: "Contrôle"
@ -1736,13 +1750,20 @@ pages:
arg1: "Listes" arg1: "Listes"
_dailyRandomPick: _dailyRandomPick:
arg1: "Listes" arg1: "Listes"
_seedRannum:
arg2: "Min"
arg3: "Max"
_seedRandomPick: _seedRandomPick:
arg2: "Listes" arg2: "Listes"
pick: "Sélectionner dans la liste"
_pick: _pick:
arg1: "Listes" arg1: "Listes"
arg2: "Position"
number: "Numérique" number: "Numérique"
stringToNumber: "Chaîne en chiffres"
_stringToNumber: _stringToNumber:
arg1: "Texte" arg1: "Texte"
numberToString: "Chiffres en chaîne"
_numberToString: _numberToString:
arg1: "Numérique" arg1: "Numérique"
_splitStrByLine: _splitStrByLine:
@ -1750,6 +1771,7 @@ pages:
ref: "Variables" ref: "Variables"
fn: "Fonction" fn: "Fonction"
_fn: _fn:
slots: "Emplacement"
arg1: "Sortie" arg1: "Sortie"
for: "Répéter" for: "Répéter"
types: types:

View File

@ -1187,7 +1187,6 @@ admin/views/index.vue:
users: "ユーザー" users: "ユーザー"
federation: "連合" federation: "連合"
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー" queue: "ジョブキュー"
logs: "ログ" logs: "ログ"
@ -1230,7 +1229,22 @@ admin/views/instance.vue:
maintainer-config: "管理者情報" maintainer-config: "管理者情報"
maintainer-name: "管理者名" maintainer-name: "管理者名"
maintainer-email: "管理者の連絡先" maintainer-email: "管理者の連絡先"
advanced-config: "その他の設定"
note-and-tl: "投稿とタイムライン"
drive-config: "ドライブの設定" drive-config: "ドライブの設定"
use-object-storage: "オブジェクトストレージを使用する"
object-storage-base-url: "URL"
object-storage-bucket: "バケット名"
object-storage-prefix: "プレフィックス"
object-storage-endpoint: "エンドポイント"
object-storage-region: "リージョン"
object-storage-port: "ポート"
object-storage-access-key: "アクセスキー"
object-storage-secret-key: "シークレットキー"
object-storage-use-ssl: "SSLを使用"
object-storage-s3-info: "Amazon S3をオブジェクトストレージとして使用する場合の「エンドポイント」と「リージョン」の設定については{0}をご確認ください。"
object-storage-s3-info-here: "こちら"
object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。"
cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
@ -1239,8 +1253,12 @@ admin/views/instance.vue:
recaptcha-config: "reCAPTCHAの設定" recaptcha-config: "reCAPTCHAの設定"
recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。" recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。"
enable-recaptcha: "reCAPTCHAを有効にする" enable-recaptcha: "reCAPTCHAを有効にする"
recaptcha-site-key: "reCAPTCHA site key" recaptcha-site-key: "サイトキー"
recaptcha-secret-key: "reCAPTCHA secret key" recaptcha-secret-key: "シークレットキー"
recaptcha-preview: "プレビュー"
hidden-tags: "非表示ハッシュタグ"
hidden-tags-info: "集計から除外するハッシュタグを改行で区切って記述します。"
external-service-integration-config: "外部サービス連携"
twitter-integration-config: "Twitter連携の設定" twitter-integration-config: "Twitter連携の設定"
twitter-integration-info: "コールバックURLは {url} に設定します。" twitter-integration-info: "コールバックURLは {url} に設定します。"
enable-twitter-integration: "Twitter連携を有効にする" enable-twitter-integration: "Twitter連携を有効にする"
@ -1361,6 +1379,8 @@ admin/views/users.vue:
unsilence-confirm: "サイレンスを解除しますか?" unsilence-confirm: "サイレンスを解除しますか?"
update-remote-user: "リモートユーザー情報の更新" update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました" remote-user-updated: "リモートユーザー情報を更新しました"
delete-all-files: "すべてのファイルを削除"
delete-all-files-confirm: "すべてのファイルを削除しますか?"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -836,7 +836,6 @@ admin/views/index.vue:
users: "ユーザー" users: "ユーザー"
federation: "連合" federation: "連合"
announcements: "知っといてや" announcements: "知っといてや"
hashtags: "ハッシュタグ"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -861,6 +860,7 @@ admin/views/instance.vue:
maintainer-name: "管理者名" maintainer-name: "管理者名"
maintainer-email: "管理者の連絡先" maintainer-email: "管理者の連絡先"
drive-config: "ドライブの設定" drive-config: "ドライブの設定"
object-storage-endpoint: "エンドポイント"
cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"

View File

@ -1065,7 +1065,6 @@ admin/views/index.vue:
users: "사용자" users: "사용자"
federation: "연합" federation: "연합"
announcements: "공지사항" announcements: "공지사항"
hashtags: "해시태그"
abuse: "스팸 신고" abuse: "스팸 신고"
queue: "작업 대기열" queue: "작업 대기열"
logs: "로그" logs: "로그"
@ -1105,6 +1104,7 @@ admin/views/instance.vue:
maintainer-name: "관리자 이름" maintainer-name: "관리자 이름"
maintainer-email: "관리자 연락처" maintainer-email: "관리자 연락처"
drive-config: "드라이브 설정" drive-config: "드라이브 설정"
object-storage-endpoint: "엔드포인트"
cache-remote-files: "원격 파일을 캐시" cache-remote-files: "원격 파일을 캐시"
cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다." cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다."
local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량" local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량"

View File

@ -855,7 +855,6 @@ admin/views/index.vue:
moderators: "Moderatorzy" moderators: "Moderatorzy"
users: "Użytkownicy" users: "Użytkownicy"
announcements: "Ogłoszenia" announcements: "Ogłoszenia"
hashtags: "Hashtagi"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Kokpit" dashboard: "Kokpit"
accounts: "Konta" accounts: "Konta"

View File

@ -145,7 +145,7 @@ common:
profile: "个人资料" profile: "个人资料"
notification: "通知" notification: "通知"
apps: "应用程序" apps: "应用程序"
tags: "标签" tags: "哈希标签"
mute-and-block: "屏蔽/拉黑" mute-and-block: "屏蔽/拉黑"
blocking: "拉黑" blocking: "拉黑"
security: "安全性" security: "安全性"
@ -168,7 +168,7 @@ common:
use-avatar-reversi-stones: "用头像作为黑白棋的棋子" use-avatar-reversi-stones: "用头像作为黑白棋的棋子"
disable-animated-mfm: "在帖子中禁用动画文本" disable-animated-mfm: "在帖子中禁用动画文本"
disable-showing-animated-images: "不播放动画" disable-showing-animated-images: "不播放动画"
suggest-recent-hashtags: "在帖子表单上显示最近流行的主题标签" suggest-recent-hashtags: "在帖子表单上显示最近流行的哈希标签"
always-show-nsfw: "总是显示 NSFW 的内容" always-show-nsfw: "总是显示 NSFW 的内容"
always-mark-nsfw: "总是用 NSFW 来标记附件" always-mark-nsfw: "总是用 NSFW 来标记附件"
show-full-acct: "不要从用户名中忽略主机名" show-full-acct: "不要从用户名中忽略主机名"
@ -297,7 +297,7 @@ common:
server: "服务器信息" server: "服务器信息"
nav: "导航" nav: "导航"
tips: "提示" tips: "提示"
hashtags: "标签" hashtags: "哈希标签"
queue: "队列" queue: "队列"
dev: "构建应用程序失败,请再试一次。" dev: "构建应用程序失败,请再试一次。"
ai-chan-kawaii: "小蓝真可爱" ai-chan-kawaii: "小蓝真可爱"
@ -469,9 +469,10 @@ common/views/components/nav.vue:
status: "状态" status: "状态"
wiki: "维基百科" wiki: "维基百科"
donors: "捐赠者" donors: "捐赠者"
repository: "码库" repository: "码库"
develop: "开发人员" develop: "开发人员"
feedback: "反馈" feedback: "反馈"
tos: "服务条款"
common/views/components/note-menu.vue: common/views/components/note-menu.vue:
mention: "提到" mention: "提到"
detail: "详细信息" detail: "详细信息"
@ -584,6 +585,8 @@ common/views/components/signup.vue:
password-matched: "确认" password-matched: "确认"
password-not-matched: "密码不一致" password-not-matched: "密码不一致"
recaptcha: "验证" recaptcha: "验证"
agree-to: "同意{0}"
tos: "服务条款"
create: "创建一个账户" create: "创建一个账户"
some-error: "由于某种原因,创建帐户失败。请再试一次。" some-error: "由于某种原因,创建帐户失败。请再试一次。"
common/views/components/special-message.vue: common/views/components/special-message.vue:
@ -713,7 +716,7 @@ common/views/widgets/posts-monitor.vue:
title: "投稿表格" title: "投稿表格"
toggle: "切换视图" toggle: "切换视图"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "标签" title: "哈希标签"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "服务器信息" title: "服务器信息"
toggle: "切换显示" toggle: "切换显示"
@ -1016,8 +1019,8 @@ desktop/views/components/timeline.vue:
mentions: "提到的" mentions: "提到的"
messages: "直接发布" messages: "直接发布"
list: "列表" list: "列表"
hashtag: "标签" hashtag: "哈希标签"
add-tag-timeline: "添加标签" add-tag-timeline: "添加哈希标签"
add-list: "添加列表" add-list: "添加列表"
list-name: "列表名称" list-name: "列表名称"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
@ -1063,7 +1066,6 @@ admin/views/index.vue:
users: "用户" users: "用户"
federation: "联合" federation: "联合"
announcements: "公告" announcements: "公告"
hashtags: "标签"
abuse: "举报垃圾信息" abuse: "举报垃圾信息"
queue: "作业队列" queue: "作业队列"
logs: "登录" logs: "登录"
@ -1090,14 +1092,34 @@ admin/views/instance.vue:
instance-name: "实例名称" instance-name: "实例名称"
instance-description: "实例介绍" instance-description: "实例介绍"
host: "主机名" host: "主机名"
icon-url: "图标URL"
logo-url: "Logo URL"
banner-url: "背景图片地址" banner-url: "背景图片地址"
error-image-url: "无效的图像URL" error-image-url: "无效的图像URL"
languages: "实例语言" languages: "实例语言"
languages-desc: "您可以添加多个,以空格分隔。" languages-desc: "您可以添加多个,以空格分隔。"
tos-url: "服务条款URL"
repository-url: "源码库URL"
feedback-url: "反馈URL"
maintainer-config: "管理员信息" maintainer-config: "管理员信息"
maintainer-name: "管理员名称" maintainer-name: "管理员名称"
maintainer-email: "联系管理员" maintainer-email: "联系管理员"
advanced-config: "其他设置"
note-and-tl: "帖子和时间线"
drive-config: "网盘设置" drive-config: "网盘设置"
use-object-storage: "使用对象存储"
object-storage-base-url: "URL"
object-storage-bucket: "存储空间名"
object-storage-prefix: "前缀"
object-storage-endpoint: "端点"
object-storage-region: "区域"
object-storage-port: "端口"
object-storage-access-key: "访问密钥"
object-storage-secret-key: "密钥"
object-storage-use-ssl: "使用 SSL"
object-storage-s3-info: "使用Amazon S3作为对象存储时请确认{0}相关“终端”和“区域”的设置。"
object-storage-s3-info-here: "这里"
object-storage-gcs-info: "将Google Cloud Storage用作对象存储时请将“终端”设置为storage.googleapis.com并将“区域”留空。"
cache-remote-files: "远程文件缓存" cache-remote-files: "远程文件缓存"
cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。" cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。"
local-drive-capacity-mb: "每个用户的网盘空间" local-drive-capacity-mb: "每个用户的网盘空间"
@ -1108,6 +1130,9 @@ admin/views/instance.vue:
enable-recaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enable-recaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptcha-site-key: "reCAPTCHA site key" recaptcha-site-key: "reCAPTCHA site key"
recaptcha-secret-key: "reCAPTCHA secret key" recaptcha-secret-key: "reCAPTCHA secret key"
hidden-tags: "隐藏哈希标签"
hidden-tags-info: "使用换行符分隔要从集合中排除的哈希标签。"
external-service-integration-config: "连接外部服务"
twitter-integration-config: "连接到Twitter的设置" twitter-integration-config: "连接到Twitter的设置"
twitter-integration-info: "设置返回的URL{url}。" twitter-integration-info: "设置返回的URL{url}。"
enable-twitter-integration: "启用连接到Twitter" enable-twitter-integration: "启用连接到Twitter"
@ -1139,6 +1164,7 @@ admin/views/instance.vue:
save: "保存" save: "保存"
saved: "保存完毕" saved: "保存完毕"
pinned-users: "置顶用户" pinned-users: "置顶用户"
pinned-users-info: "描述您要置顶的用户,以换行符分隔。"
email-config: "电子邮件服务器设置" email-config: "电子邮件服务器设置"
email-config-info: "用于确认电子邮件和密码重置等。" email-config-info: "用于确认电子邮件和密码重置等。"
enable-email: "启用电子邮件送递" enable-email: "启用电子邮件送递"
@ -1224,6 +1250,8 @@ admin/views/users.vue:
unsilence-confirm: "解除屏蔽?" unsilence-confirm: "解除屏蔽?"
update-remote-user: "更新远程用户信息" update-remote-user: "更新远程用户信息"
remote-user-updated: "远程用户信息已更新" remote-user-updated: "远程用户信息已更新"
delete-all-files: "删除所有文件"
delete-all-files-confirm: "删除所有文件吗?"
users: users:
title: "用户" title: "用户"
sort: sort:
@ -1299,6 +1327,7 @@ admin/views/federation.vue:
latest-request-received-at: "上次收到的请求" latest-request-received-at: "上次收到的请求"
remove-all-following: "取消所有关注" remove-all-following: "取消所有关注"
remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。" remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。"
delete-all-files: "删除所有文件"
block: "拉黑" block: "拉黑"
marked-as-closed: "标记为已关闭" marked-as-closed: "标记为已关闭"
lookup: "查询" lookup: "查询"
@ -1346,6 +1375,7 @@ admin/views/federation.vue:
hour: "每小时" hour: "每小时"
day: "每天" day: "每天"
blocked-hosts: "拉黑" blocked-hosts: "拉黑"
blocked-hosts-info: "描述您要阻止的主机,以换行符分隔。"
desktop/views/pages/welcome.vue: desktop/views/pages/welcome.vue:
about: "更多信息..." about: "更多信息..."
timeline: "时间线" timeline: "时间线"
@ -1367,7 +1397,7 @@ desktop/views/pages/search.vue:
not-available: "在此实例的设置中关闭搜索功能。" not-available: "在此实例的设置中关闭搜索功能。"
not-found: "没有找到“{q}”的帖子" not-found: "没有找到“{q}”的帖子"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子" no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
users: "用户" users: "用户"
add-user: "添加用户" add-user: "添加用户"
@ -1443,7 +1473,7 @@ mobile/views/components/drive.file-detail.vue:
download: "下载" download: "下载"
rename: "重命名" rename: "重命名"
move: "移动" move: "移动"
hash: "Hash (md5)" hash: "哈希(md5)"
exif: "EXIF" exif: "EXIF"
nsfw: "阅读注意" nsfw: "阅读注意"
mark-as-sensitive: "标记为“敏感”" mark-as-sensitive: "标记为“敏感”"
@ -1530,7 +1560,7 @@ mobile/views/pages/home.vue:
mentions: "Mentions" mentions: "Mentions"
messages: "直接发布" messages: "直接发布"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子" no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "仪表盘" dashboard: "仪表盘"
widgets-hints: "您可以添加/删除/重新排列小部件。 要移动小部件,请拖动“三”。 点击“×”删除小部件。 某些小部件可以通过点击来更改显示。" widgets-hints: "您可以添加/删除/重新排列小部件。 要移动小部件,请拖动“三”。 点击“×”删除小部件。 某些小部件可以通过点击来更改显示。"
@ -1582,7 +1612,7 @@ deck:
home: "首页" home: "首页"
local: "Local" local: "Local"
hybrid: "社交" hybrid: "社交"
hashtag: "标签" hashtag: "哈希标签"
global: "Global" global: "Global"
mentions: "Mentions" mentions: "Mentions"
direct: "直接发布" direct: "直接发布"

View File

@ -0,0 +1,31 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class ObjectStorageSetting1557932705754 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`);
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.13.0", "version": "11.15.0",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",
@ -12,6 +12,9 @@
"scripts": { "scripts": {
"start": "node ./index.js", "start": "node ./index.js",
"init": "node ./built/init.js", "init": "node ./built/init.js",
"ormconfig": "node ./built/ormconfig.js",
"migrate": "npm run ormconfig && ts-node ./node_modules/typeorm/cli.js migration:run",
"migrateandstart": "npm run migrate && npm run start",
"build": "webpack && gulp build", "build": "webpack && gulp build",
"webpack": "webpack", "webpack": "webpack",
"watch": "webpack --watch", "watch": "webpack --watch",

View File

@ -1,41 +0,0 @@
<template>
<div>
<ui-card>
<template #title>{{ $t('hided-tags') }}</template>
<section>
<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea>
<ui-button @click="save">{{ $t('save') }}</ui-button>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
export default Vue.extend({
i18n: i18n('admin/views/hashtags.vue'),
data() {
return {
hiddenTags: '',
};
},
created() {
this.$root.getMeta().then(meta => {
this.hiddenTags = meta.hiddenTags.join('\n');
});
},
methods: {
save() {
this.$root.api('admin/update-meta', {
hiddenTags: this.hiddenTags.split('\n')
}).then(() => {
//this.$root.os.apis.dialog({ text: `Saved` });
}).catch(e => {
//this.$root.os.apis.dialog({ text: `Failed ${e}` });
});
}
}
});
</script>

View File

@ -28,7 +28,6 @@
<li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</li> <li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</li>
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li> <li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li> <li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
<li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li> <li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li>
</ul> </ul>
<div class="back-to-misskey"> <div class="back-to-misskey">
@ -48,7 +47,6 @@
<div v-if="page == 'users'"><x-users/></div> <div v-if="page == 'users'"><x-users/></div>
<div v-if="page == 'emoji'"><x-emoji/></div> <div v-if="page == 'emoji'"><x-emoji/></div>
<div v-if="page == 'announcements'"><x-announcements/></div> <div v-if="page == 'announcements'"><x-announcements/></div>
<div v-if="page == 'hashtags'"><x-hashtags/></div>
<div v-if="page == 'drive'"><x-drive/></div> <div v-if="page == 'drive'"><x-drive/></div>
<div v-if="page == 'federation'"><x-federation/></div> <div v-if="page == 'federation'"><x-federation/></div>
<div v-if="page == 'abuse'"><x-abuse/></div> <div v-if="page == 'abuse'"><x-abuse/></div>
@ -68,7 +66,6 @@ import XLogs from "./logs.vue";
import XModerators from "./moderators.vue"; import XModerators from "./moderators.vue";
import XEmoji from "./emoji.vue"; import XEmoji from "./emoji.vue";
import XAnnouncements from "./announcements.vue"; import XAnnouncements from "./announcements.vue";
import XHashtags from "./hashtags.vue";
import XUsers from "./users.vue"; import XUsers from "./users.vue";
import XDrive from "./drive.vue"; import XDrive from "./drive.vue";
import XAbuse from "./abuse.vue"; import XAbuse from "./abuse.vue";
@ -91,7 +88,6 @@ export default Vue.extend({
XModerators, XModerators,
XEmoji, XEmoji,
XAnnouncements, XAnnouncements,
XHashtags,
XUsers, XUsers,
XDrive, XDrive,
XAbuse, XAbuse,

View File

@ -2,7 +2,7 @@
<div> <div>
<ui-card> <ui-card>
<template #title><fa icon="cog"/> {{ $t('instance') }}</template> <template #title><fa icon="cog"/> {{ $t('instance') }}</template>
<section class="fit-top fit-bottom"> <section class="fit-top">
<ui-input :value="host" readonly>{{ $t('host') }}</ui-input> <ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
<ui-input v-model="name">{{ $t('instance-name') }}</ui-input> <ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea> <ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
@ -11,77 +11,83 @@
<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input> <ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input> <ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
<ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input> <ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input>
<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
<ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input> <ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input>
<details>
<summary>{{ $t('advanced-config') }}</summary>
<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
</details>
</section> </section>
<section class="fit-bottom"> <section class="fit-bottom">
<header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header> <header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header>
<ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input> <ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input>
<ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input> <ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input>
</section> </section>
<section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
<ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template>
<section class="fit-top fit-bottom"> <section class="fit-top fit-bottom">
<ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input> <ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input>
</section> </section>
<section> <section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> <ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch> <ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
<ui-info>{{ $t('disabling-timelines-info') }}</ui-info> <ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
</section>
<section>
<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch> <ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
<ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch> <ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch>
</section> </section>
<section class="fit-bottom"> <section>
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header> <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template>
<section>
<ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch>
<template v-if="useObjectStorage">
<ui-info>
<i18n path="object-storage-s3-info">
<a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a>
</i18n>
</ui-info>
<ui-info>{{ $t('object-storage-gcs-info') }}</ui-info>
<ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input>
<ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input>
</ui-horizon-group>
<ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input>
<ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input>
<ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch>
</template>
</section>
<section>
<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch>
</section>
<section class="fit-top fit-bottom">
<ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
<ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> <ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
</section> </section>
<section class="fit-bottom">
<header><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</header>
<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
<ui-info>{{ $t('recaptcha-info') }}</ui-info>
<ui-horizon-group inputs>
<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
</ui-horizon-group>
</section>
<section> <section>
<header><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</header> <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
<ui-info>{{ $t('proxy-account-info') }}</ui-info>
<ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input>
<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
</section>
<section>
<header><fa :icon="farEnvelope"/> {{ $t('email-config') }}</header>
<ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch>
<ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch>
<ui-horizon-group inputs>
<ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input>
<ui-input v-model="smtpPass" type="password" :withPasswordToggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
</section>
<section>
<header><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</header>
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch>
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
<ui-horizon-group inputs class="fit-bottom">
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input>
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input>
</ui-horizon-group>
</section>
<section>
<header>summaly Proxy</header>
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
@ -91,56 +97,146 @@
<ui-textarea v-model="pinnedUsers"> <ui-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinned-users-info') }}</template> <template #desc>{{ $t('pinned-users-info') }}</template>
</ui-textarea> </ui-textarea>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
<ui-card> <ui-card>
<template #title>{{ $t('invite') }}</template> <template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template>
<section> <section>
<ui-button @click="invite">{{ $t('invite') }}</ui-button> <ui-info>{{ $t('proxy-account-info') }}</ui-info>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> <ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input>
<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
<ui-card> <ui-card>
<template #title><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</template> <template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template>
<section> <section>
<ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch>
<template v-if="enableEmail">
<ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch>
<ui-horizon-group inputs>
<ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input>
<ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template>
<section>
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch>
<template v-if="enableServiceWorker">
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
<ui-horizon-group inputs class="fit-bottom">
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input>
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input>
</ui-horizon-group>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template>
<section :class="enableRecaptcha ? 'fit-bottom' : ''">
<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
<template v-if="enableRecaptcha">
<ui-info>{{ $t('recaptcha-info') }}</ui-info>
<ui-horizon-group inputs>
<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
</ui-horizon-group>
</template>
</section>
<section v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('recaptcha-preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template>
<section>
<header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header>
<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
<ui-horizon-group> <template v-if="enableTwitterIntegration">
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input> <ui-horizon-group>
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input> <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input>
</ui-horizon-group> <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
<ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info> </ui-horizon-group>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
</template>
</section> </section>
</ui-card>
<ui-card>
<template #title><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</template>
<section> <section>
<header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header>
<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
<ui-horizon-group> <template v-if="enableGithubIntegration">
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input> <ui-horizon-group>
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input> <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input>
</ui-horizon-group> <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input>
<ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info> </ui-horizon-group>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
</template>
</section>
<section>
<header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header>
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
<template v-if="enableDiscordIntegration">
<ui-horizon-group>
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input>
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
<ui-card> <details>
<template #title><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</template> <summary style="color:var(--text);">{{ $t('advanced-config') }}</summary>
<section>
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> <ui-card>
<ui-horizon-group> <template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template>
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input> <section class="fit-top">
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input> <ui-textarea v-model="hiddenTags">
</ui-horizon-group> <template #desc>{{ $t('hidden-tags-info') }}</template>
<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info> </ui-textarea>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
<ui-card>
<template #title>summaly Proxy</template>
<section class="fit-top fit-bottom">
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
</details>
</div> </div>
</template> </template>
@ -149,8 +245,8 @@ import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { url, host } from '../../config'; import { url, host } from '../../config';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack } from '@fortawesome/free-solid-svg-icons'; import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons';
import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons'; import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/instance.vue'), i18n: i18n('admin/views/instance.vue'),
@ -193,7 +289,6 @@ export default Vue.extend({
discordClientId: null, discordClientId: null,
discordClientSecret: null, discordClientSecret: null,
proxyAccount: null, proxyAccount: null,
inviteCode: null,
summalyProxy: null, summalyProxy: null,
enableEmail: false, enableEmail: false,
email: null, email: null,
@ -207,7 +302,18 @@ export default Vue.extend({
swPublicKey: null, swPublicKey: null,
swPrivateKey: null, swPrivateKey: null,
pinnedUsers: '', pinnedUsers: '',
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack hiddenTags: '',
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag
}; };
}, },
@ -260,13 +366,55 @@ export default Vue.extend({
this.swPublicKey = meta.swPublickey; this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey; this.swPrivateKey = meta.swPrivateKey;
this.pinnedUsers = meta.pinnedUsers.join('\n'); this.pinnedUsers = meta.pinnedUsers.join('\n');
this.hiddenTags = meta.hiddenTags.join('\n');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;
this.objectStoragePrefix = meta.objectStoragePrefix;
this.objectStorageEndpoint = meta.objectStorageEndpoint;
this.objectStorageRegion = meta.objectStorageRegion;
this.objectStoragePort = meta.objectStoragePort;
this.objectStorageAccessKey = meta.objectStorageAccessKey;
this.objectStorageSecretKey = meta.objectStorageSecretKey;
this.objectStorageUseSSL = meta.objectStorageUseSSL;
});
},
mounted() {
const renderRecaptchaPreview = () => {
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return;
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, {
sitekey: this.recaptchaSiteKey
});
};
window.onRecaotchaLoad = () => {
renderRecaptchaPreview();
};
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
head.appendChild(script);
this.$watch('enableRecaptcha', () => {
renderRecaptchaPreview();
});
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview();
}); });
}, },
methods: { methods: {
invite() { invite() {
this.$root.api('admin/invite').then(x => { this.$root.api('admin/invite').then(x => {
this.inviteCode = x.code; this.$root.dialog({
type: 'info',
text: x.code
});
}).catch(e => { }).catch(e => {
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
@ -322,7 +470,18 @@ export default Vue.extend({
enableServiceWorker: this.enableServiceWorker, enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey, swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey, swPrivateKey: this.swPrivateKey,
pinnedUsers: this.pinnedUsers.split('\n') pinnedUsers: this.pinnedUsers.split('\n'),
hiddenTags: this.hiddenTags.split('\n'),
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
}).then(() => { }).then(() => {
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',

View File

@ -9,8 +9,9 @@
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<div class="user" v-if="user"> <div class="user" v-if="user">
<x-user :user='user'/> <x-user :user="user"/>
<div class="actions"> <div class="actions">
<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group> <ui-horizon-group>
<ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button> <ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button>
@ -20,7 +21,7 @@
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group> </ui-horizon-group>
<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button> <ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</div> </div>
</div> </div>
@ -67,7 +68,7 @@ import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse"; import parseAcct from "../../../../misc/acct/parse";
import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import XUser from './users.user.vue'; import XUser from './users.user.vue';
export default Vue.extend({ export default Vue.extend({
@ -88,7 +89,7 @@ export default Vue.extend({
offset: 0, offset: 0,
users: [], users: [],
existMore: false, existMore: false,
faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt
}; };
}, },
@ -277,6 +278,25 @@ export default Vue.extend({
this.refreshUser(); this.refreshUser();
}, },
async deleteAllFiles() {
if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return;
const process = async () => {
await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
splash: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
},
async getConfirmed(text: string): Promise<Boolean> { async getConfirmed(text: string): Promise<Boolean> {
const confirm = await this.$root.dialog({ const confirm = await this.$root.dialog({
type: 'warning', type: 'warning',

View File

@ -27,13 +27,6 @@ export type Source = {
port: number; port: number;
pass: string; pass: string;
}; };
drive?: {
storage: string;
bucket?: string;
prefix?: string;
baseUrl?: string;
config?: any;
};
autoAdmin?: boolean; autoAdmin?: boolean;

View File

@ -288,4 +288,61 @@ export class Meta {
nullable: true nullable: true
}) })
public feedbackUrl: string | null; public feedbackUrl: string | null;
@Column('boolean', {
default: false,
})
public useObjectStorage: boolean;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBucket: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStoragePrefix: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBaseUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageEndpoint: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageRegion: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageAccessKey: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageSecretKey: string | null;
@Column('integer', {
nullable: true
})
public objectStoragePort: number | null;
@Column('boolean', {
default: true,
})
public objectStorageUseSSL: boolean;
} }

18
src/ormconfig.ts Normal file
View File

@ -0,0 +1,18 @@
import * as fs from 'fs';
import config from './config';
const json = {
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
entities: ['src/models/entities/*.ts'],
migrations: ['migration/*.ts'],
cli: {
migrationsDir: 'migration'
}
};
fs.writeFileSync('ormconfig.json', JSON.stringify(json));

View File

@ -0,0 +1,32 @@
import $ from 'cafy';
import define from '../../define';
import del from '../../../../services/drive/delete-file';
import { DriveFiles } from '../../../../models';
import { ID } from '../../../../misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
}
},
}
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userId: ps.userId
});
for (const file of files) {
del(file);
}
});

View File

@ -53,16 +53,18 @@ export default define(meta, async (ps) => {
if (blackDomains.length > 0) { if (blackDomains.length > 0) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
for (const blackDomain of blackDomains) { for (const blackDomain of blackDomains) {
const subDomains = blackDomain.split('.'); qb.andWhere(new Brackets(qb => {
let i = 0; const subDomains = blackDomain.split('.');
for (const subDomain of subDomains) { let i = 0;
const p = `blackSubDomain_${subDomain}_${i}`; for (const subDomain of subDomains) {
// 全体で否定できないのでド・モルガンの法則で const p = `blackSubDomain_${subDomain}_${i}`;
// !(P && Q) を !P || !Q で表す // 全体で否定できないのでド・モルガンの法則で
// SQL is 1 based, so we need '+ 1' // !(P && Q) を !P || !Q で表す
qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); // SQL is 1 based, so we need '+ 1'
i++; qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
} i++;
}
}));
} }
})); }));
} }

View File

@ -357,7 +357,47 @@ export const meta = {
desc: { desc: {
'ja-JP': 'フィードバックのURL' 'ja-JP': 'フィードバックのURL'
} }
} },
useObjectStorage: {
validator: $.optional.bool
},
objectStorageBaseUrl: {
validator: $.optional.nullable.str
},
objectStorageBucket: {
validator: $.optional.nullable.str
},
objectStoragePrefix: {
validator: $.optional.nullable.str
},
objectStorageEndpoint: {
validator: $.optional.nullable.str
},
objectStorageRegion: {
validator: $.optional.nullable.str
},
objectStoragePort: {
validator: $.optional.nullable.num
},
objectStorageAccessKey: {
validator: $.optional.nullable.str
},
objectStorageSecretKey: {
validator: $.optional.nullable.str
},
objectStorageUseSSL: {
validator: $.optional.bool
},
} }
}; };
@ -560,6 +600,46 @@ export default define(meta, async (ps) => {
set.feedbackUrl = ps.feedbackUrl; set.feedbackUrl = ps.feedbackUrl;
} }
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}
if (ps.objectStorageBaseUrl !== undefined) {
set.objectStorageBaseUrl = ps.objectStorageBaseUrl;
}
if (ps.objectStorageBucket !== undefined) {
set.objectStorageBucket = ps.objectStorageBucket;
}
if (ps.objectStoragePrefix !== undefined) {
set.objectStoragePrefix = ps.objectStoragePrefix;
}
if (ps.objectStorageEndpoint !== undefined) {
set.objectStorageEndpoint = ps.objectStorageEndpoint;
}
if (ps.objectStorageRegion !== undefined) {
set.objectStorageRegion = ps.objectStorageRegion;
}
if (ps.objectStoragePort !== undefined) {
set.objectStoragePort = ps.objectStoragePort;
}
if (ps.objectStorageAccessKey !== undefined) {
set.objectStorageAccessKey = ps.objectStorageAccessKey;
}
if (ps.objectStorageSecretKey !== undefined) {
set.objectStorageSecretKey = ps.objectStorageSecretKey;
}
if (ps.objectStorageUseSSL !== undefined) {
set.objectStorageUseSSL = ps.objectStorageUseSSL;
}
await getConnection().transaction(async transactionalEntityManager => { await getConnection().transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, { const meta = await transactionalEntityManager.findOne(Meta, {
order: { order: {

View File

@ -153,7 +153,7 @@ export default define(meta, async (ps, me) => {
globalTimeLine: !instance.disableGlobalTimeline, globalTimeLine: !instance.disableGlobalTimeline,
elasticsearch: config.elasticsearch ? true : false, elasticsearch: config.elasticsearch ? true : false,
recaptcha: instance.enableRecaptcha, recaptcha: instance.enableRecaptcha,
objectStorage: config.drive && config.drive.storage === 'minio', objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration, twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration, github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration, discord: instance.enableDiscordIntegration,
@ -182,6 +182,16 @@ export default define(meta, async (ps, me) => {
response.smtpUser = instance.smtpUser; response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass; response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey; response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
} }
return response; return response;

View File

@ -1,7 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as Koa from 'koa'; import * as Koa from 'koa';
import { serverLogger } from '..'; import { serverLogger } from '..';
import { IImage, ConvertToPng, ConvertToJpeg } from '../../services/drive/image-processor'; import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor';
import { createTemp } from '../../misc/create-temp'; import { createTemp } from '../../misc/create-temp';
import { downloadUrl } from '../../misc/donwload-url'; import { downloadUrl } from '../../misc/donwload-url';
import { detectMine } from '../../misc/detect-mine'; import { detectMine } from '../../misc/detect-mine';
@ -20,9 +20,9 @@ export async function proxyMedia(ctx: Koa.BaseContext) {
let image: IImage; let image: IImage;
if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) { if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) {
image = await ConvertToPng(path, 498, 280); image = await convertToPng(path, 498, 280);
} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) { } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) {
image = await ConvertToJpeg(path, 200, 200); image = await convertToJpeg(path, 200, 200);
} else { } else {
image = { image = {
data: fs.readFileSync(path), data: fs.readFileSync(path),

View File

@ -8,11 +8,10 @@ import * as sharp from 'sharp';
import { publishMainStream, publishDriveStream } from '../stream'; import { publishMainStream, publishDriveStream } from '../stream';
import delFile from './delete-file'; import delFile from './delete-file';
import config from '../../config';
import { fetchMeta } from '../../misc/fetch-meta'; import { fetchMeta } from '../../misc/fetch-meta';
import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { GenerateVideoThumbnail } from './generate-video-thumbnail';
import { driveLogger } from './logger'; import { driveLogger } from './logger';
import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; import { IImage, convertToJpeg, convertToWebp, convertToPng, convertToGif, convertToApng } from './image-processor';
import { contentDisposition } from '../../misc/content-disposition'; import { contentDisposition } from '../../misc/content-disposition';
import { detectMine } from '../../misc/detect-mine'; import { detectMine } from '../../misc/detect-mine';
import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models';
@ -37,7 +36,9 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
// thunbnail, webpublic を必要なら生成 // thunbnail, webpublic を必要なら生成
const alts = await generateAlts(path, type, !file.uri); const alts = await generateAlts(path, type, !file.uri);
if (config.drive && config.drive.storage == 'minio') { const meta = await fetchMeta();
if (meta.useObjectStorage) {
//#region ObjectStorage params //#region ObjectStorage params
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
@ -47,11 +48,11 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
if (type === 'image/webp') ext = '.webp'; if (type === 'image/webp') ext = '.webp';
} }
const baseUrl = config.drive.baseUrl const baseUrl = meta.objectStorageBaseUrl
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; || `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
// for original // for original
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; const key = `${meta.objectStoragePrefix}/${uuid.v4()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${ baseUrl }/${ key }`;
// for alts // for alts
@ -68,7 +69,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
]; ];
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${alts.webpublic.ext}`; webpublicKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
logger.info(`uploading webpublic: ${webpublicKey}`); logger.info(`uploading webpublic: ${webpublicKey}`);
@ -76,7 +77,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
} }
if (alts.thumbnail) { if (alts.thumbnail) {
thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${alts.thumbnail.ext}`; thumbnailKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
logger.info(`uploading thumbnail: ${thumbnailKey}`); logger.info(`uploading thumbnail: ${thumbnailKey}`);
@ -149,11 +150,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
logger.info(`creating web image`); logger.info(`creating web image`);
if (['image/jpeg'].includes(type)) { if (['image/jpeg'].includes(type)) {
webpublic = await ConvertToJpeg(path, 2048, 2048); webpublic = await convertToJpeg(path, 2048, 2048);
} else if (['image/webp'].includes(type)) { } else if (['image/webp'].includes(type)) {
webpublic = await ConvertToWebp(path, 2048, 2048); webpublic = await convertToWebp(path, 2048, 2048);
} else if (['image/png'].includes(type)) { } else if (['image/png'].includes(type)) {
webpublic = await ConvertToPng(path, 2048, 2048); webpublic = await convertToPng(path, 2048, 2048);
} else if (['image/apng', 'image/vnd.mozilla.apng'].includes(type)) {
webpublic = await convertToApng(path);
} else if (['image/gif'].includes(type)) {
webpublic = await convertToGif(path);
} else { } else {
logger.info(`web image not created (not an image)`); logger.info(`web image not created (not an image)`);
} }
@ -166,9 +171,11 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
let thumbnail: IImage | null = null; let thumbnail: IImage | null = null;
if (['image/jpeg', 'image/webp'].includes(type)) { if (['image/jpeg', 'image/webp'].includes(type)) {
thumbnail = await ConvertToJpeg(path, 498, 280); thumbnail = await convertToJpeg(path, 498, 280);
} else if (['image/png'].includes(type)) { } else if (['image/png'].includes(type)) {
thumbnail = await ConvertToPng(path, 498, 280); thumbnail = await convertToPng(path, 498, 280);
} else if (['image/gif'].includes(type)) {
thumbnail = await convertToGif(path);
} else if (type.startsWith('video/')) { } else if (type.startsWith('video/')) {
try { try {
thumbnail = await GenerateVideoThumbnail(path); thumbnail = await GenerateVideoThumbnail(path);
@ -188,7 +195,16 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
* Upload to ObjectStorage * Upload to ObjectStorage
*/ */
async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) {
const minio = new Minio.Client(config.drive!.config); const meta = await fetchMeta();
const minio = new Minio.Client({
endPoint: meta.objectStorageEndpoint!,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined,
port: meta.objectStoragePort ? meta.objectStoragePort : undefined,
useSSL: meta.objectStorageUseSSL,
accessKey: meta.objectStorageAccessKey!,
secretKey: meta.objectStorageSecretKey!,
});
const metadata = { const metadata = {
'Content-Type': type, 'Content-Type': type,
@ -197,7 +213,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename); if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename);
await minio.putObject(config.drive!.bucket!, key, stream, undefined, metadata); await minio.putObject(meta.objectStorageBucket!, key, stream, undefined, metadata);
} }
async function deleteOldFile(user: IRemoteUser) { async function deleteOldFile(user: IRemoteUser) {

View File

@ -1,9 +1,9 @@
import * as Minio from 'minio'; import * as Minio from 'minio';
import config from '../../config';
import { DriveFile } from '../../models/entities/drive-file'; import { DriveFile } from '../../models/entities/drive-file';
import { InternalStorage } from './internal-storage'; import { InternalStorage } from './internal-storage';
import { DriveFiles, Instances, Notes } from '../../models'; import { DriveFiles, Instances, Notes } from '../../models';
import { driveChart, perUserDriveChart, instanceChart } from '../chart'; import { driveChart, perUserDriveChart, instanceChart } from '../chart';
import { fetchMeta } from '../../misc/fetch-meta';
export default async function(file: DriveFile, isExpired = false) { export default async function(file: DriveFile, isExpired = false) {
if (file.storedInternal) { if (file.storedInternal) {
@ -17,16 +17,25 @@ export default async function(file: DriveFile, isExpired = false) {
InternalStorage.del(file.webpublicAccessKey!); InternalStorage.del(file.webpublicAccessKey!);
} }
} else if (!file.isLink) { } else if (!file.isLink) {
const minio = new Minio.Client(config.drive!.config); const meta = await fetchMeta();
await minio.removeObject(config.drive!.bucket!, file.accessKey!); const minio = new Minio.Client({
endPoint: meta.objectStorageEndpoint!,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined,
port: meta.objectStoragePort ? meta.objectStoragePort : undefined,
useSSL: meta.objectStorageUseSSL,
accessKey: meta.objectStorageAccessKey!,
secretKey: meta.objectStorageSecretKey!,
});
await minio.removeObject(meta.objectStorageBucket!, file.accessKey!);
if (file.thumbnailUrl) { if (file.thumbnailUrl) {
await minio.removeObject(config.drive!.bucket!, file.thumbnailAccessKey!); await minio.removeObject(meta.objectStorageBucket!, file.thumbnailAccessKey!);
} }
if (file.webpublicUrl) { if (file.webpublicUrl) {
await minio.removeObject(config.drive!.bucket!, file.webpublicAccessKey!); await minio.removeObject(meta.objectStorageBucket!, file.webpublicAccessKey!);
} }
} }

View File

@ -1,6 +1,6 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as tmp from 'tmp'; import * as tmp from 'tmp';
import { IImage, ConvertToJpeg } from './image-processor'; import { IImage, convertToJpeg } from './image-processor';
const ThumbnailGenerator = require('video-thumbnail-generator').default; const ThumbnailGenerator = require('video-thumbnail-generator').default;
export async function GenerateVideoThumbnail(path: string): Promise<IImage> { export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
@ -23,7 +23,7 @@ export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
const outPath = `${outDir}/output.png`; const outPath = `${outDir}/output.png`;
const thumbnail = await ConvertToJpeg(outPath, 498, 280); const thumbnail = await convertToJpeg(outPath, 498, 280);
// cleanup // cleanup
fs.unlinkSync(outPath); fs.unlinkSync(outPath);

View File

@ -1,4 +1,5 @@
import * as sharp from 'sharp'; import * as sharp from 'sharp';
import * as fs from 'fs';
export type IImage = { export type IImage = {
data: Buffer; data: Buffer;
@ -10,7 +11,7 @@ export type IImage = {
* Convert to JPEG * Convert to JPEG
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
*/ */
export async function ConvertToJpeg(path: string, width: number, height: number): Promise<IImage> { export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
const data = await sharp(path) const data = await sharp(path)
.resize(width, height, { .resize(width, height, {
fit: 'inside', fit: 'inside',
@ -34,7 +35,7 @@ export async function ConvertToJpeg(path: string, width: number, height: number)
* Convert to WebP * Convert to WebP
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
*/ */
export async function ConvertToWebp(path: string, width: number, height: number): Promise<IImage> { export async function convertToWebp(path: string, width: number, height: number): Promise<IImage> {
const data = await sharp(path) const data = await sharp(path)
.resize(width, height, { .resize(width, height, {
fit: 'inside', fit: 'inside',
@ -57,7 +58,7 @@ export async function ConvertToWebp(path: string, width: number, height: number)
* Convert to PNG * Convert to PNG
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
*/ */
export async function ConvertToPng(path: string, width: number, height: number): Promise<IImage> { export async function convertToPng(path: string, width: number, height: number): Promise<IImage> {
const data = await sharp(path) const data = await sharp(path)
.resize(width, height, { .resize(width, height, {
fit: 'inside', fit: 'inside',
@ -73,3 +74,29 @@ export async function ConvertToPng(path: string, width: number, height: number):
type: 'image/png' type: 'image/png'
}; };
} }
/**
* Convert to GIF (Actually just NOP)
*/
export async function convertToGif(path: string): Promise<IImage> {
const data = await fs.promises.readFile(path);
return {
data,
ext: 'gif',
type: 'image/gif'
};
}
/**
* Convert to APNG (Actually just NOP)
*/
export async function convertToApng(path: string): Promise<IImage> {
const data = await fs.promises.readFile(path);
return {
data,
ext: 'apng',
type: 'image/apng'
};
}