diff --git a/CHANGELOG.md b/CHANGELOG.md index e7499f32a..e9319075c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,20 @@ unreleased ---------- * 返信するときにCWを維持するかどうか設定できるように * 外部サービス認証情報の配信 +* 管理画面のモデレーションのUIを強化 +* 管理画面からリモートユーザーの情報を更新できるように +* 回転構文の追加 +* 左右反転構文の追加 +* シンタックスハイライトの強化 +* 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正 * イタリック構文の判定の改善 +* タイトル構文の判定の改善 * テーマが反映されないことがある問題を修正 * ホームにフォロワー限定投稿が表示されない問題を修正 * 返信一覧を取得すると非公開投稿も取得されてしまう問題を修正 * メンション一覧を取得すると非公開投稿も取得されてしまう問題を修正 * 通知に非公開投稿が表示される問題を修正 +* ダイレクトで投稿すると100%の確率で表示が二重になる問題を修正 * ウィジットの投稿フォームで投稿するとデフォルトの公開範囲が適用されない問題を修正 10.78.5 diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 339a66d0c..3901a4e90 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 225fc70c0..0be4b072b 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/en-US.yml b/locales/en-US.yml index 84fb4a170..5c14e3fc4 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -807,8 +807,8 @@ desktop/views/components/settings.vue: timeline: "Timeline" show-my-renotes: "Show my renotes in the timeline" show-renoted-my-notes: "Show renoted posts of mine in timelines" - show-local-renotes: "Show renoted local posts in timelines" - show-maps: "Display a map to show the location" + show-local-renotes: "Show renoted local posts in the timelines" + show-maps: "Display a map to show location" remain-deleted-note: "Continue to show deleted posts" deck-column-align: "Deck column alignment" deck-column-align-center: "Center" @@ -1135,15 +1135,22 @@ admin/views/users.vue: user-not-found: "User not found" lookup: "Look up" reset-password: "Reset password" + reset-password-confirm: "Do you want to reset your password?" password-updated: "The password is now \"{password}\"" suspend: "Suspend" + suspend-confirm: "Do you want to suspend this account?" suspended: "Successfully suspended." unsuspend: "Unsuspend" + unsuspend-confirm: "Are you sure you want to unsuspend this account?" unsuspended: "The user has successfully unsuspended." verify: "Verify account" + verify-confirm: "Do you want this to be a verified account?" verified: "The account is now being verified" unverify: "Unverify account" + unverify-confirm: "Do you want to remove the 'verified account' designation?" unverified: "The account is now being unverified" + update-remote-user: "Update information about remote user" + remote-user-updated: "The information regarding the remote user has been updated." users: title: "Users" sort: diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 0f6a887b0..8ace707e5 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -11,7 +11,7 @@ common: about: "Misskey es un Servicio de red social descentralizada de microblogging de código abierto. Contiene una interfaz de usuario altamente personalizable, reacciones a posts, almacenamiento para poder manejar archivos y otras funciones avanzadas. Además de conectarse con la red llamada Fediverso, puede intercambiar mensajes con otras redes sociales. Por ejemplo, si contribuyes con algo, esa contribución es transmitida no sólo a Misskey sino a otras redes sociales. Imagina que se parece a transmitir una onda de radio de un planeta a otro." features: "Características" rich-contents: "Posts" - rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" + rich-contents-desc: "Escribe sobre tus pensamientos, eventos, todo lo que quieras compartir. Si es necesario, puedes usar varias sintaxis, decorar tus posts y añadir tus imágenes favoritas, archivos de viddeo y encuestas." reaction: "Reacciones" reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" ui: "Interfaz" @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index e0216e9a7..c097872de 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -788,7 +788,7 @@ desktop/views/components/settings.vue: auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" deck-nav: "Deck sans tansitions" deck-nav-desc: "Vous obtenez une colonne temporaire sans transitions dans la page pendant la navigation, lors de l’utilisation du Deck." - keep-cw: "CW保持" + keep-cw: "Maintenir l'avertissement de contenu" keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。" deck-default: "Utiliser le Deck comme IU par défaut" display: "Affichage et design" @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "Utilisateur non trouvé" lookup: "Recherche" reset-password: "Réinitialiser mot de passe" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "Le mot de passe est « {password} »" suspend: "Suspendre" + suspend-confirm: "Désirez-vous suspendre ce compte ?" suspended: "Suspendu avec succès." unsuspend: "Suspension levée" + unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?" unsuspended: "La suspension de l’utilisateur a été levée avec succès" verify: "Vérification du compte" + verify-confirm: "Souhaiteriez-vous rendre votre compte comme étant un compte vérifié ?" verified: "Le compte a été vérifié" unverify: "Enlever la vérification du compte" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "Ce compte n'est plus vérifié" + update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e" + remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour" users: title: "Utilisateurs" sort: @@ -1469,7 +1476,7 @@ mobile/views/pages/settings.vue: notification-position-top: "en haut" behavior: "Comportement" fetch-on-scroll: "Chargement lors du défilement" - keep-cw: "CW保持" + keep-cw: "Garder l'avertissement de contenu" note-visibility: "Visibilité de la publication" default-note-visibility: "Visibilité par défaut" remember-note-visibility: "Se souvenir du mode de visibilité de la publication" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index b826757cc..92a3ec84c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index bae7a1173..91a6add5d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -730,10 +730,6 @@ desktop/views/components/drive.vue: upload: "ファイルをアップロード" url-upload: "URLからアップロード" -desktop/views/components/media-image.vue: - sensitive: "閲覧注意" - click-to-show: "クリックして表示" - desktop/views/components/media-video.vue: sensitive: "閲覧注意" click-to-show: "クリックして表示" @@ -980,6 +976,10 @@ desktop/views/components/settings.2fa.vue: failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。" +common/views/components/media-image.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + common/views/components/api-settings.vue: intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" @@ -1266,15 +1266,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: @@ -1486,10 +1493,6 @@ mobile/views/components/drive.file-detail.vue: mark-as-sensitive: "閲覧注意に設定" unmark-as-sensitive: "閲覧注意を解除" -mobile/views/components/media-image.vue: - sensitive: "閲覧注意" - click-to-show: "クリックして表示" - mobile/views/components/media-video.vue: sensitive: "閲覧注意" click-to-show: "クリックして表示" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index add497eb9..d3270f1aa 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つからへん!" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password} 」やで" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 70c41d525..bb04a67d0 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "사용자를 찾을 수 없습니다" lookup: "조회" reset-password: "암호 재설정" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "암호는 현재 \"{password}\" 입니다" suspend: "정지" + suspend-confirm: "凍結しますか?" suspended: "정지하였습니다" unsuspend: "정지 해제" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "정지를 해제하였습니다" verify: "공식 계정으로 설정" + verify-confirm: "公式アカウントにしますか?" verified: "공식 계정으로 설정하였습니다" unverify: "공식 계정 해제" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "공식 계정을 해제하였습니다" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "사용자" sort: diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 14252b3ef..d6f2c8789 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/no-NO.yml b/locales/no-NO.yml index b272f834c..ffade4a24 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index d2680194a..ec1c6214e 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "Nie znaleziono użytkownika" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "Użytkownicy" sort: diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 61fa70986..27549d3f7 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 235ae3b3d..2f7980dcb 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "ユーザー" sort: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index b197f16dc..a93b8c798 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1134,15 +1134,22 @@ admin/views/users.vue: user-not-found: "用户不存在" lookup: "订阅" reset-password: "密码重置" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "密码为「{password}」" suspend: "被冻结" + suspend-confirm: "凍結しますか?" suspended: "成功冻结用户" unsuspend: "已解除冻结" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "已成功解除用户冻结" verify: "认证用户" + verify-confirm: "公式アカウントにしますか?" verified: "此账户已被认证" unverify: "解除账户认证" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "该帐户未经认证" + update-remote-user: "リモートユーザー情報の更新" + remote-user-updated: "リモートユーザー情報を更新しました" users: title: "用户" sort: diff --git a/package.json b/package.json index 5f7c704fb..b561b4de4 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ "bootstrap-vue": "2.0.0-rc.11", "cafy": "12.0.0", "chai": "4.2.0", - "chalk": "2.4.2", "chai-http": "4.2.1", + "chalk": "2.4.2", "commander": "2.19.0", "crc-32": "1.2.0", "css-loader": "1.0.1", @@ -178,13 +178,14 @@ "parsimmon": "1.12.0", "portscanner": "2.2.0", "postcss-loader": "3.0.0", + "prismjs": "1.15.0", "progress-bar-webpack-plugin": "1.12.0", "promise-any": "0.2.0", "promise-limit": "2.7.0", "promise-sequential": "1.1.1", "pug": "2.0.3", "punycode": "2.1.1", - "qrcode": "1.3.2", + "qrcode": "1.3.3", "randomcolor": "0.5.3", "ratelimiter": "3.2.0", "recaptcha-promise": "0.1.3", @@ -230,6 +231,7 @@ "vue-js-modal": "1.3.28", "vue-loader": "15.5.1", "vue-marquee-text-component": "1.1.1", + "vue-prism-component": "1.1.1", "vue-router": "3.0.2", "vue-sequential-entrance": "1.1.3", "vue-style-loader": "4.1.2", diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue new file mode 100644 index 000000000..afece18e8 --- /dev/null +++ b/src/client/app/admin/views/users.user.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index 6f0f1629f..09d074eee 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -3,20 +3,27 @@
{{ $t('operation') }}
- + {{ $t('username-or-userid') }} - {{ $t('reset-password') }} - - {{ $t('verify') }} - {{ $t('unverify') }} - - - {{ $t('suspend') }} - {{ $t('unsuspend') }} - {{ $t('lookup') }} - + +
+ +
+ {{ $t('reset-password') }} + + {{ $t('verify') }} + {{ $t('unverify') }} + + + {{ $t('suspend') }} + {{ $t('unsuspend') }} + + {{ $t('update-remote-user') }} + +
+
@@ -47,29 +54,7 @@ -
-
- - - -
-
-
- - @{{ user | acct }} - admin - moderator - - -
-
- {{ $t('users.updatedAt') }}: -
-
- {{ $t('users.createdAt') }}: -
-
-
+
{{ $t('@.load-more') }} @@ -81,12 +66,15 @@ import Vue from 'vue'; import i18n from '../../i18n'; import parseAcct from "../../../../misc/acct/parse"; -import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons'; +import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync } from '@fortawesome/free-solid-svg-icons'; import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; +import XUser from './users.user.vue'; export default Vue.extend({ i18n: i18n('admin/views/users.vue'), - + components: { + XUser + }, data() { return { user: null, @@ -102,7 +90,7 @@ export default Vue.extend({ offset: 0, users: [], existMore: false, - faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey + faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync }; }, @@ -131,6 +119,7 @@ export default Vue.extend({ }, methods: { + /** テキストエリアのユーザーを解決する */ async fetchUser() { try { return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target }); @@ -149,16 +138,27 @@ export default Vue.extend({ } }, + /** テキストエリアから処理対象ユーザーを設定する */ async showUser() { + this.user = null; const user = await this.fetchUser(); this.$root.api('admin/show-user', { userId: user.id }).then(info => { this.user = info; }); + this.target = ''; + }, + + /** 処理対象ユーザーの情報を更新する */ + async refreshUser() { + this.$root.api('admin/show-user', { userId: this.user._id }).then(info => { + this.user = info; + }); }, async resetPassword() { - const user = await this.fetchUser(); - this.$root.api('admin/reset-password', { userId: user.id }).then(res => { + if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; + + this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('password-updated', { password: res.password }) @@ -167,11 +167,12 @@ export default Vue.extend({ }, async verifyUser() { + if (!await this.getConfirmed(this.$t('verify-confirm'))) return; + this.verifying = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/verify-user', { userId: user.id }); + await this.$root.api('admin/verify-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('verified') @@ -186,14 +187,17 @@ export default Vue.extend({ }); this.verifying = false; + + this.refreshUser(); }, async unverifyUser() { + if (!await this.getConfirmed(this.$t('unverify-confirm'))) return; + this.unverifying = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/unverify-user', { userId: user.id }); + await this.$root.api('admin/unverify-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('unverified') @@ -208,14 +212,17 @@ export default Vue.extend({ }); this.unverifying = false; + + this.refreshUser(); }, async suspendUser() { + if (!await this.getConfirmed(this.$t('suspend-confirm'))) return; + this.suspending = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/suspend-user', { userId: user.id }); + await this.$root.api('admin/suspend-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('suspended') @@ -230,14 +237,17 @@ export default Vue.extend({ }); this.suspending = false; + + this.refreshUser(); }, async unsuspendUser() { + if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return; + this.unsuspending = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/unsuspend-user', { userId: user.id }); + await this.$root.api('admin/unsuspend-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('unsuspended') @@ -252,8 +262,32 @@ export default Vue.extend({ }); this.unsuspending = false; + + this.refreshUser(); }, + async updateRemoteUser() { + this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => { + this.$root.dialog({ + type: 'success', + text: this.$t('remote-user-updated') + }); + }); + + this.refreshUser(); + }, + + async getConfirmed(text: string): Promise { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + title: 'confirm', + text, + }); + + return !confirm.canceled; + } + fetchUsers() { this.$root.api('admin/show-users', { state: this.state, @@ -277,42 +311,12 @@ export default Vue.extend({ diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl index a62916520..9cbd3ec6c 100644 --- a/src/client/app/animation.styl +++ b/src/client/app/animation.styl @@ -26,3 +26,8 @@ transform: translateY(0); } } + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/client/app/app.styl b/src/client/app/app.styl index 13b013328..a98ece7a5 100644 --- a/src/client/app/app.styl +++ b/src/client/app/app.styl @@ -72,47 +72,6 @@ body code font-family Consolas, 'Courier New', Courier, Monaco, monospace - .comment - opacity 0.5 - - .string - color #e96900 - - .regexp - color #e9003f - - .keyword - color #2973b7 - - &.true - &.false - &.null - &.nil - &.undefined - color #ae81ff - - .symbol - color #42b983 - - .number - .nan - color #ae81ff - - .var:not(.keyword) - font-weight bold - font-style italic - //text-decoration underline - - .method - font-style italic - color #8964c1 - - .property - color #a71d5d - - .label - color #e9003f - pre display block diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts index 9545b5406..c2b4dd6df 100644 --- a/src/client/app/common/scripts/note-subscriber.ts +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -133,6 +133,7 @@ export default prop => ({ case 'deleted': { Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt); + Vue.set(this.$_ns_target, 'renote', null); this.$_ns_target.text = null; this.$_ns_target.tags = []; this.$_ns_target.fileIds = []; diff --git a/src/client/app/common/views/components/code-core.vue b/src/client/app/common/views/components/code-core.vue new file mode 100644 index 000000000..a50d94394 --- /dev/null +++ b/src/client/app/common/views/components/code-core.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/client/app/common/views/components/code.vue b/src/client/app/common/views/components/code.vue new file mode 100644 index 000000000..d52c9f7bc --- /dev/null +++ b/src/client/app/common/views/components/code.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue similarity index 84% rename from src/client/app/mobile/views/components/media-image.vue rename to src/client/app/common/views/components/media-image.vue index dbb275b51..01187465f 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/common/views/components/media-image.vue @@ -5,16 +5,21 @@ {{ $t('click-to-show') }} - + - diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue index e5ea7bea1..8633b86e4 100644 --- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue +++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue @@ -1,17 +1,19 @@ @@ -36,79 +38,19 @@ export default Vue.extend({ }); - diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts index 8ea2b602d..2edc8117a 100644 --- a/src/client/app/desktop/views/components/index.ts +++ b/src/client/app/desktop/views/components/index.ts @@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue'; import window from './window.vue'; import noteFormWindow from './post-form-window.vue'; import renoteFormWindow from './renote-form-window.vue'; -import mediaImage from './media-image.vue'; import mediaVideo from './media-video.vue'; import notifications from './notifications.vue'; import noteForm from './post-form.vue'; @@ -32,7 +31,6 @@ Vue.component('mk-sub-note-content', subNoteContent); Vue.component('mk-window', window); Vue.component('mk-post-form-window', noteFormWindow); Vue.component('mk-renote-form-window', renoteFormWindow); -Vue.component('mk-media-image', mediaImage); Vue.component('mk-media-video', mediaVideo); Vue.component('mk-notifications', notifications); Vue.component('mk-post-form', noteForm); diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue deleted file mode 100644 index 446e50093..000000000 --- a/src/client/app/desktop/views/components/media-image.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 9a410e827..351aaea9f 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -3,7 +3,6 @@ import Vue from 'vue'; import ui from './ui.vue'; import note from './note.vue'; import notes from './notes.vue'; -import mediaImage from './media-image.vue'; import mediaVideo from './media-video.vue'; import notePreview from './note-preview.vue'; import subNoteContent from './sub-note-content.vue'; @@ -24,7 +23,6 @@ import postForm from './post-form.vue'; Vue.component('mk-ui', ui); Vue.component('mk-note', note); Vue.component('mk-notes', notes); -Vue.component('mk-media-image', mediaImage); Vue.component('mk-media-video', mediaVideo); Vue.component('mk-note-preview', notePreview); Vue.component('mk-sub-note-content', subNoteContent); diff --git a/src/docs/style.styl b/src/docs/style.styl index 4af0f288b..96d14c2b9 100644 --- a/src/docs/style.styl +++ b/src/docs/style.styl @@ -3,6 +3,8 @@ html --primary #fb4e4e + --link #fb4e4e + --linkTapHighlight #fb4e4eb3 body margin 0 diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts index a198e8dd2..bb27d6f80 100644 --- a/src/games/reversi/core.ts +++ b/src/games/reversi/core.ts @@ -100,20 +100,6 @@ export default class Reversi { return count(WHITE, this.board); } - /** - * 黒石の比率 - */ - public get blackP() { - return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount); - } - - /** - * 白石の比率 - */ - public get whiteP() { - return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount); - } - public transformPosToXy(pos: number): number[] { const x = pos % this.mapWidth; const y = Math.floor(pos / this.mapWidth); diff --git a/src/mfm/html.ts b/src/mfm/html.ts index b86d33a39..acd6891ab 100644 --- a/src/mfm/html.ts +++ b/src/mfm/html.ts @@ -55,6 +55,18 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU return el; }, + spin(token) { + const el = doc.createElement('i'); + appendChildren(token.children, el); + return el; + }, + + flip(token) { + const el = doc.createElement('span'); + appendChildren(token.children, el); + return el; + }, + blockCode(token) { const pre = doc.createElement('pre'); const inner = doc.createElement('code'); diff --git a/src/mfm/parser.ts b/src/mfm/parser.ts index 01b69c969..7bddea2ac 100644 --- a/src/mfm/parser.ts +++ b/src/mfm/parser.ts @@ -91,6 +91,7 @@ const mfm = P.createLanguage({ root: r => P.alt( r.big, r.small, + r.spin, r.bold, r.strike, r.italic, @@ -101,6 +102,7 @@ const mfm = P.createLanguage({ r.hashtag, r.emoji, r.blockCode, + r.flip, r.inlineCode, r.quote, r.mathInline, @@ -123,6 +125,7 @@ const mfm = P.createLanguage({ r.hashtag, r.emoji, r.mathInline, + r.spin, r.text ).atLeast(1).tryParse(x), {})), //#endregion @@ -141,6 +144,15 @@ const mfm = P.createLanguage({ ).atLeast(1).tryParse(x), {})), //#endregion + //#region Spin + spin: r => + P.regexp(/(.+?)<\/spin>/, 1) + .map(x => createTree('spin', P.alt( + r.emoji, + r.text + ).atLeast(1).tryParse(x), {})), + //#endregion + //#region Block code blockCode: r => newline.then( @@ -163,6 +175,7 @@ const mfm = P.createLanguage({ r.hashtag, r.url, r.link, + r.flip, r.emoji, r.text ).atLeast(1).tryParse(x), {})), @@ -174,6 +187,7 @@ const mfm = P.createLanguage({ .map(x => createTree('center', P.alt( r.big, r.small, + r.spin, r.bold, r.strike, r.italic, @@ -184,6 +198,7 @@ const mfm = P.createLanguage({ r.mathInline, r.url, r.link, + r.flip, r.text ).atLeast(1).tryParse(x), {})), //#endregion @@ -217,6 +232,23 @@ const mfm = P.createLanguage({ }), //#endregion + //#region Flip + flip: r => + P.regexp(/(.+?)<\/flip>/, 1) + .map(x => createTree('flip', P.alt( + r.big, + r.small, + r.spin, + r.bold, + r.strike, + r.link, + r.italic, + r.motion, + r.emoji, + r.text + ).atLeast(1).tryParse(x), {})), + //#endregion + //#region Inline code inlineCode: r => P.regexp(/`([^´\n]+?)`/, 1) @@ -242,6 +274,7 @@ const mfm = P.createLanguage({ r.hashtag, r.url, r.link, + r.flip, r.emoji, r.text ).atLeast(1).tryParse(x), {})), @@ -262,6 +295,7 @@ const mfm = P.createLanguage({ return createTree('link', P.alt( r.big, r.small, + r.spin, r.bold, r.strike, r.italic, @@ -311,6 +345,7 @@ const mfm = P.createLanguage({ .map(x => createTree('motion', P.alt( r.bold, r.small, + r.spin, r.strike, r.italic, r.mention, @@ -318,6 +353,7 @@ const mfm = P.createLanguage({ r.emoji, r.url, r.link, + r.flip, r.mathInline, r.text ).atLeast(1).tryParse(x), {})), @@ -356,6 +392,7 @@ const mfm = P.createLanguage({ r.hashtag, r.url, r.link, + r.flip, r.emoji, r.text ).atLeast(1).tryParse(x), {})), @@ -365,18 +402,20 @@ const mfm = P.createLanguage({ title: r => newline.then(P((input, i) => { const text = input.substr(i); - const match = text.match(/^((【|\[)(.+?)(】|]))(\n|$)/); + const match = text.match(/^([【\[]([^【\[】\]\n]+?)[】\]])(\n|$)/); if (!match) return P.makeFailure(i, 'not a title'); - const q = match[1].trim().substring(1, match[1].length - 1); + const q = match[2].trim(); const contents = P.alt( r.big, r.small, + r.spin, r.bold, r.strike, r.italic, r.motion, r.url, r.link, + r.flip, r.mention, r.hashtag, r.emoji, diff --git a/src/mfm/syntax-highlight.ts b/src/mfm/syntax-highlight.ts deleted file mode 100644 index 109923fb7..000000000 --- a/src/mfm/syntax-highlight.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { capitalize, toUpperCase } from '../prelude/string'; - -function escape(text: string) { - return text - .replace(/>/g, '>') - .replace(/ b.length - a.length); - -const symbols = [ - '=', - '+', - '-', - '*', - '/', - '%', - '~', - '^', - '&', - '|', - '>', - '<', - '!', - '?' -]; - -type Token = { - html: string - next: number -}; - -type Element = (code: string, i: number, source: string) => (Token | null); - -const elements: Element[] = [ - // comment - code => { - if (code.substr(0, 2) != '//') return null; - const match = code.match(/^\/\/(.+?)(\n|$)/); - if (!match) return null; - const comment = match[0]; - return { - html: `${escape(comment)}`, - next: comment.length - }; - }, - - // block comment - code => { - const match = code.match(/^\/\*([\s\S]+?)\*\//); - if (!match) return null; - return { - html: `${escape(match[0])}`, - next: match[0].length - }; - }, - - // string - code => { - if (!/^['"`]/.test(code)) return null; - const begin = code[0]; - let str = begin; - let thisIsNotAString = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - str += char; - str += code[i + 1] || ''; - i++; - continue; - } else if (char == begin) { - str += char; - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotAString = true; - break; - } else { - str += char; - } - } - if (thisIsNotAString) { - return null; - } else { - return { - html: `${escape(str)}`, - next: str.length - }; - } - }, - - // regexp - code => { - if (code[0] != '/') return null; - let regexp = ''; - let thisIsNotARegexp = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - regexp += char; - regexp += code[i + 1] || ''; - i++; - continue; - } else if (char == '/') { - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotARegexp = true; - break; - } else { - regexp += char; - } - } - - if (thisIsNotARegexp) return null; - if (regexp == '') return null; - if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null; - - return { - html: `/${escape(regexp)}/`, - next: regexp.length + 2 - }; - }, - - // label - code => { - if (code[0] != '@') return null; - const match = code.match(/^@([a-zA-Z_-]+?)\n/); - if (!match) return null; - const label = match[0]; - return { - html: `${label}`, - next: label.length - }; - }, - - // number - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (!/^[\-\+]?[0-9\.]+/.test(code)) return null; - const match = code.match(/^[\-\+]?[0-9\.]+/)[0]; - if (match) { - return { - html: `${match}`, - next: match.length - }; - } else { - return null; - } - }, - - // nan - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (code.substr(0, 3) == 'NaN') { - return { - html: `NaN`, - next: 3 - }; - } else { - return null; - } - }, - - // method - code => { - const match = code.match(/^([a-zA-Z_-]+?)\(/); - if (!match) return null; - - if (match[1] == '-') return null; - - return { - html: `${match[1]}`, - next: match[1].length - }; - }, - - // property - (code, i, source) => { - const prev = source[i - 1]; - if (prev != '.') return null; - - const match = code.match(/^[a-zA-Z0-9_-]+/); - if (!match) return null; - - return { - html: `${match[0]}`, - next: match[0].length - }; - }, - - // keyword - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - - const match = keywords.filter(k => code.substr(0, k.length) == k)[0]; - if (match) { - if (/^[a-zA-Z]/.test(code.substr(match.length))) return null; - return { - html: `${match}`, - next: match.length - }; - } else { - return null; - } - }, - - // symbol - code => { - const match = symbols.filter(s => code[0] == s)[0]; - if (match) { - return { - html: `${match}`, - next: 1 - }; - } else { - return null; - } - } -]; - -// TODO: specify lang -export default (source: string, lang?: string): string => { - let code = source; - let html = ''; - - let i = 0; - - function push(token: Token) { - html += token.html; - code = code.substr(token.next); - i += token.next; - } - - while (code != '') { - const parsed = elements.some(el => { - const e = el(code, i, source); - if (e) { - push(e); - return true; - } else { - return false; - } - }); - - if (!parsed) { - push({ - html: escape(code[0]), - next: 1 - }); - } - } - - return html; -}; diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts index 1fce58b20..1cd054cab 100644 --- a/src/server/api/common/getters.ts +++ b/src/server/api/common/getters.ts @@ -1,5 +1,6 @@ import * as mongo from 'mongodb'; import Note from "../../../models/note"; +import User, { isRemoteUser, isLocalUser } from "../../../models/user"; /** * Get valied note for API processing @@ -16,3 +17,44 @@ export async function getValiedNote(noteId: mongo.ObjectID) { return note; } + +/** + * Get user for API processing + */ +export async function getUser(userId: mongo.ObjectID) { + const user = await User.findOne({ + _id: userId + }); + + if (user == null) { + throw 'user not found'; + } + + return user; +} + +/** + * Get remote user for API processing + */ +export async function getRemoteUser(userId: mongo.ObjectID) { + const user = await getUser(userId); + + if (!isRemoteUser(user)) { + throw 'user is not a remote user'; + } + + return user; +} + +/** + * Get local user for API processing + */ +export async function getLocalUser(userId: mongo.ObjectID) { + const user = await getUser(userId); + + if (!isLocalUser(user)) { + throw 'user is not a local user'; + } + + return user; +} diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts new file mode 100644 index 000000000..9288ce1fb --- /dev/null +++ b/src/server/api/endpoints/admin/update-remote-user.ts @@ -0,0 +1,36 @@ +import * as mongo from 'mongodb'; +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { getRemoteUser } from '../../common/getters'; +import { updatePerson } from '../../../../remote/activitypub/models/person'; + +export const meta = { + desc: { + 'ja-JP': '指定されたリモートユーザーの情報を更新します。', + 'en-US': 'Update specified remote user information.' + }, + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to update' + } + }, + } +}; + +export default define(meta, (ps) => new Promise((res, rej) => { + updatePersonById(ps.userId).then(() => res(), e => rej(e)); +})); + +async function updatePersonById(userId: mongo.ObjectID) { + const user = await getRemoteUser(userId); + await updatePerson(user.uri); +} diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts index b520b29e2..19beee433 100644 --- a/src/server/api/endpoints/users/report-abuse.ts +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; import define from '../../define'; import User from '../../../../models/user'; import AbuseUserReport from '../../../../models/abuse-user-report'; +import { publishAdminStream } from '../../../../stream'; export const meta = { desc: { @@ -47,12 +48,31 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { return rej('cannot report admin'); } - await AbuseUserReport.insert({ + const report = await AbuseUserReport.insert({ createdAt: new Date(), userId: user._id, reporterId: me._id, comment: ps.comment }); + // Publish event to moderators + setTimeout(async () => { + const moderators = await User.find({ + $or: [{ + isAdmin: true + }, { + isModerator: true + }] + }); + for (const moderator of moderators) { + publishAdminStream(moderator._id, 'newAbuseUserReport', { + id: report._id, + userId: report.userId, + reporterId: report.reporterId, + comment: report.comment + }); + } + }, 1); + res(); })); diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts new file mode 100644 index 000000000..6bcd1a7e0 --- /dev/null +++ b/src/server/api/stream/channels/admin.ts @@ -0,0 +1,16 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; + +export default class extends Channel { + public readonly chName = 'admin'; + public static shouldShare = true; + public static requireCredential = true; + + @autobind + public async init(params: any) { + // Subscribe admin stream + this.subscriber.on(`adminStream:${this.user._id}`, data => { + this.send(data); + }); + } +} diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts index 7248579ab..02f71b585 100644 --- a/src/server/api/stream/channels/index.ts +++ b/src/server/api/stream/channels/index.ts @@ -11,6 +11,7 @@ import messagingIndex from './messaging-index'; import drive from './drive'; import hashtag from './hashtag'; import apLog from './ap-log'; +import admin from './admin'; import gamesReversi from './games/reversi'; import gamesReversiGame from './games/reversi-game'; @@ -28,6 +29,7 @@ export default { drive, hashtag, apLog, + admin, gamesReversi, gamesReversiGame }; diff --git a/src/services/note/create.ts b/src/services/note/create.ts index d3c8699b2..3b5aac8f8 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -377,8 +377,10 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren if (note.visibility == 'specified') { for (const u of visibleUsers) { - publishHomeTimelineStream(u._id, detailPackedNote); - publishHybridTimelineStream(u._id, detailPackedNote); + if (!u._id.equals(user._id)) { + publishHomeTimelineStream(u._id, detailPackedNote); + publishHybridTimelineStream(u._id, detailPackedNote); + } } } } else { diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index 9709eeaf5..e8ce181d5 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -30,12 +30,25 @@ export default async function(user: IUser, note: INote) { text: null, tags: [], fileIds: [], + renoteId: null, poll: null, geo: null, cw: null } }); + if (note.renoteId) { + Note.update({ _id: note.renoteId }, { + $inc: { + renoteCount: -1, + score: -1 + }, + $pull: { + _quoteIds: note._id + } + }); + } + publishNoteStream(note._id, 'deleted', { deletedAt: deletedAt }); diff --git a/src/stream.ts b/src/stream.ts index 596cb98e7..098d49ecd 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -87,6 +87,10 @@ class Publisher { public publishApLogStream = (log: any): void => { this.publish('apLog', null, log); } + + public publishAdminStream = (userId: ID, type: string, value?: any): void => { + this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } } const publisher = new Publisher(); @@ -107,3 +111,4 @@ export const publishHybridTimelineStream = publisher.publishHybridTimelineStream export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; export const publishHashtagStream = publisher.publishHashtagStream; export const publishApLogStream = publisher.publishApLogStream; +export const publishAdminStream = publisher.publishAdminStream; diff --git a/src/tools/resync-remote-user.ts b/src/tools/resync-remote-user.ts index c013de723..4850c768a 100644 --- a/src/tools/resync-remote-user.ts +++ b/src/tools/resync-remote-user.ts @@ -24,9 +24,7 @@ if (!acct.match(/^\w+@\w/)) { console.log(`resync ${acct}`); main(acct).then(() => { - console.log('success'); - process.exit(0); + console.log('Done'); }).catch(e => { console.warn(e); - process.exit(1); }); diff --git a/test/mfm.ts b/test/mfm.ts index f850e649a..7070329f3 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -152,9 +152,19 @@ describe('MFM', () => { it('can be analyzed', () => { const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'); assert.deepStrictEqual(tokens, [ - leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }), + leaf('mention', { + acct: '@himawari', + canonical: '@himawari', + username: 'himawari', + host: null + }), text(' '), - leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }), + leaf('mention', { + acct: '@hima_sub@namori.net', + canonical: '@hima_sub@namori.net', + username: 'hima_sub', + host: 'namori.net' + }), text(' お腹ペコい '), leaf('emoji', { name: 'cat' }), text(' '), @@ -234,6 +244,24 @@ describe('MFM', () => { ]); }); + it('flip', () => { + const tokens = analyze('foo'); + assert.deepStrictEqual(tokens, [ + tree('flip', [ + text('foo') + ], {}), + ]); + }); + + it('spin', () => { + const tokens = analyze(':foo:'); + assert.deepStrictEqual(tokens, [ + tree('spin', [ + leaf('emoji', { name: 'foo' }) + ], {}), + ]); + }); + describe('motion', () => { it('by triple brackets', () => { const tokens = analyze('(((foo)))'); @@ -280,7 +308,12 @@ describe('MFM', () => { it('local', () => { const tokens = analyze('@himawari foo'); assert.deepStrictEqual(tokens, [ - leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }), + leaf('mention', { + acct: '@himawari', + canonical: '@himawari', + username: 'himawari', + host: null + }), text(' foo') ]); }); @@ -288,7 +321,12 @@ describe('MFM', () => { it('remote', () => { const tokens = analyze('@hima_sub@namori.net foo'); assert.deepStrictEqual(tokens, [ - leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }), + leaf('mention', { + acct: '@hima_sub@namori.net', + canonical: '@hima_sub@namori.net', + username: 'hima_sub', + host: 'namori.net' + }), text(' foo') ]); }); @@ -296,7 +334,12 @@ describe('MFM', () => { it('remote punycode', () => { const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo'); assert.deepStrictEqual(tokens, [ - leaf('mention', { acct: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' }), + leaf('mention', { + acct: '@hima_sub@xn--q9j5bya.xn--zckzah', + canonical: '@hima_sub@なもり.テスト', + username: 'hima_sub', + host: 'xn--q9j5bya.xn--zckzah' + }), text(' foo') ]); }); @@ -309,11 +352,26 @@ describe('MFM', () => { const tokens2 = analyze('@a\n@b\n@c'); assert.deepStrictEqual(tokens2, [ - leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }), + leaf('mention', { + acct: '@a', + canonical: '@a', + username: 'a', + host: null + }), text('\n'), - leaf('mention', { acct: '@b', canonical: '@b', username: 'b', host: null }), + leaf('mention', { + acct: '@b', + canonical: '@b', + username: 'b', + host: null + }), text('\n'), - leaf('mention', { acct: '@c', canonical: '@c', username: 'c', host: null }) + leaf('mention', { + acct: '@c', + canonical: '@c', + username: 'c', + host: null + }) ]); const tokens3 = analyze('**x**@a'); @@ -321,24 +379,31 @@ describe('MFM', () => { tree('bold', [ text('x') ], {}), - leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }) + leaf('mention', { + acct: '@a', + canonical: '@a', + username: 'a', + host: null + }) ]); - const tokens4 = analyze('@\n@v\n@veryverylongusername' /* \n@toolongtobeasamention */); + const tokens4 = analyze('@\n@v\n@veryverylongusername'); assert.deepStrictEqual(tokens4, [ text('@\n'), - leaf('mention', { acct: '@v', canonical: '@v', username: 'v', host: null }), + leaf('mention', { + acct: '@v', + canonical: '@v', + username: 'v', + host: null + }), text('\n'), - leaf('mention', { acct: '@veryverylongusername', canonical: '@veryverylongusername', username: 'veryverylongusername', host: null }), - // text('\n@toolongtobeasamention') + leaf('mention', { + acct: '@veryverylongusername', + canonical: '@veryverylongusername', + username: 'veryverylongusername', + host: null + }), ]); - /* - const tokens5 = analyze('@domain_is@valid.example.com\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com'); - assert.deepStrictEqual([ - leaf('mention', { acct: '@domain_is@valid.example.com', canonical: '@domain_is@valid.example.com', username: 'domain_is', host: 'valid.example.com' }), - text('\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com') - ], tokens5); - */ }); }); @@ -905,6 +970,20 @@ describe('MFM', () => { text('after') ]); }); + + it('ignore multiple title blocks', () => { + const tokens = analyze('【foo】bar【baz】'); + assert.deepStrictEqual(tokens, [ + text('【foo】bar【baz】') + ]); + }); + + it('disallow linebreak in title', () => { + const tokens = analyze('【foo\nbar】'); + assert.deepStrictEqual(tokens, [ + text('【foo\nbar】') + ]); + }); }); describe('center', () => {