From 7f665daaf3c0d173d2ceaee282df67698db7a561 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 28 Dec 2021 11:08:39 +0100 Subject: [PATCH 01/16] Handle actor refreshment being impossible If we can't refresh the actor, just return the stale actor Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/actor.ex | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/federation/activity_pub/actor.ex b/lib/federation/activity_pub/actor.ex index 30f141a9..42bd31d8 100644 --- a/lib/federation/activity_pub/actor.ex +++ b/lib/federation/activity_pub/actor.ex @@ -33,7 +33,10 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do case Actors.get_actor_by_url(url, preload) do {:ok, %Actor{} = cached_actor} -> if Actors.needs_update?(cached_actor) do - __MODULE__.make_actor_from_url(url, options) + case __MODULE__.make_actor_from_url(url, options) do + {:ok, %Actor{} = actor} -> {:ok, actor} + {:error, _} -> {:ok, cached_actor} + end else {:ok, cached_actor} end @@ -83,11 +86,14 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do Logger.debug("Finding or making actor from nickname #{nickname}") case Actors.get_actor_by_name_with_preload(nickname, type) do - %Actor{url: actor_url} = actor -> - if Actors.needs_update?(actor) do - make_actor_from_url(actor_url, preload: true) + %Actor{url: actor_url} = cached_actor -> + if Actors.needs_update?(cached_actor) do + case __MODULE__.make_actor_from_url(actor_url, preload: true) do + {:ok, %Actor{} = actor} -> {:ok, actor} + {:error, _} -> {:ok, cached_actor} + end else - {:ok, actor} + {:ok, cached_actor} end nil -> From 65249b60f2d8f2d85249995acade05792a12fca1 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 28 Dec 2021 11:10:14 +0100 Subject: [PATCH 02/16] Add TailwindCSS Signed-off-by: Thomas Citharel --- js/package.json | 4 + js/postcss.config.js | 6 ++ js/src/assets/tailwind.css | 5 ++ js/src/main.ts | 1 + js/src/variables.scss | 12 +++ js/tailwind.config.js | 21 +++++ js/yarn.lock | 156 ++++++++++++++++++++++++++++++++++--- 7 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 js/postcss.config.js create mode 100644 js/src/assets/tailwind.css create mode 100644 js/tailwind.config.js diff --git a/js/package.json b/js/package.json index b73cf3fc..9dd10184 100644 --- a/js/package.json +++ b/js/package.json @@ -41,6 +41,7 @@ "@vue-a11y/skip-to": "^2.1.2", "@vue/apollo-option": "4.0.0-alpha.11", "apollo-absinthe-upload-link": "^1.5.0", + "autoprefixer": "^10", "blurhash": "^1.1.3", "buefy": "^0.9.0", "bulma-divider": "^0.2.0", @@ -57,8 +58,10 @@ "ngeohash": "^0.6.3", "p-debounce": "^4.0.0", "phoenix": "^1.6", + "postcss": "^8", "register-service-worker": "^1.7.2", "sanitize-html": "^2.5.3", + "tailwindcss": "^3", "tippy.js": "^6.2.3", "unfetch": "^4.2.0", "v-tooltip": "^2.1.3", @@ -112,6 +115,7 @@ "sass-loader": "^12.0.0", "ts-jest": "27", "typescript": "~4.4.3", + "vue-cli-plugin-tailwind": "^3.0.0-beta.0", "vue-i18n-extract": "^2.0.4", "vue-template-compiler": "^2.6.11", "webpack-cli": "^4.7.0" diff --git a/js/postcss.config.js b/js/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/js/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/js/src/assets/tailwind.css b/js/src/assets/tailwind.css new file mode 100644 index 00000000..7f393742 --- /dev/null +++ b/js/src/assets/tailwind.css @@ -0,0 +1,5 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; diff --git a/js/src/main.ts b/js/src/main.ts index c3e513bd..317f2b37 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -13,6 +13,7 @@ import filters from "./filters"; import { i18n } from "./utils/i18n"; import apolloProvider from "./vue-apollo"; import "./registerServiceWorker"; +import "./assets/tailwind.css"; Vue.config.productionTip = false; diff --git a/js/src/variables.scss b/js/src/variables.scss index 46c03bcb..6c52cdcb 100644 --- a/js/src/variables.scss +++ b/js/src/variables.scss @@ -139,3 +139,15 @@ $subtitle-sup-size: 15px; $breadcrumb-item-color: $primary; $checkbox-background-color: #fff; $title-color: $violet-3; + +:root { + --color-primary: 30 125 151; + --color-secondary: 255 213 153; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-primary: 30 125 151; + --color-secondary: 255 213 153; + } +} diff --git a/js/tailwind.config.js b/js/tailwind.config.js new file mode 100644 index 00000000..fa03e280 --- /dev/null +++ b/js/tailwind.config.js @@ -0,0 +1,21 @@ +function withOpacityValue(variable) { + return ({ opacityValue }) => { + if (opacityValue === undefined) { + return `rgb(var(${variable}))`; + } + return `rgb(var(${variable}) / ${opacityValue})`; + }; +} + +module.exports = { + content: ["./public/**/*.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + primary: withOpacityValue("--color-primary"), + secondary: withOpacityValue("--color-secondary"), + }, + }, + }, + plugins: [], +}; diff --git a/js/yarn.lock b/js/yarn.lock index 86a53cf6..bd3ad50b 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -2765,7 +2765,16 @@ acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^7.1.1: +acorn-node@^1.6.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0, acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== @@ -2775,7 +2784,7 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -2960,6 +2969,11 @@ arch@^2.1.1: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +arg@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" + integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3039,7 +3053,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^10.2.4: +autoprefixer@^10, autoprefixer@^10.2.4: version "10.4.0" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8" integrity sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA== @@ -3390,6 +3404,11 @@ camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -3605,7 +3624,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -3791,7 +3810,7 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^7.0.0: +cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== @@ -4094,6 +4113,11 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= + del@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" @@ -4133,6 +4157,20 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +detective@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== + dependencies: + acorn-node "^1.6.1" + defined "^1.0.0" + minimist "^1.1.1" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + diff-sequences@^27.4.0: version "27.4.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" @@ -4145,7 +4183,7 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dlv@^1.1.0: +dlv@^1.1.0, dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== @@ -5144,7 +5182,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: +glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -5487,6 +5525,13 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== +import-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" + integrity sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg== + dependencies: + import-from "^3.0.0" + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -5495,6 +5540,13 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966" + integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== + dependencies: + resolve-from "^5.0.0" + import-local@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.3.tgz#4d51c2c495ca9393da259ec66b62e022920211e0" @@ -6875,7 +6927,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -7088,6 +7140,11 @@ object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.11.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -7490,6 +7547,23 @@ postcss-discard-overridden@^5.0.1: resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== +postcss-js@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-3.0.3.tgz#2f0bd370a2e8599d45439f6970403b5873abda33" + integrity sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw== + dependencies: + camelcase-css "^2.0.1" + postcss "^8.1.6" + +postcss-load-config@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.0.tgz#d39c47091c4aec37f50272373a6a648ef5e97829" + integrity sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g== + dependencies: + import-cwd "^3.0.0" + lilconfig "^2.0.3" + yaml "^1.10.2" + postcss-loader@^6.1.1: version "6.2.1" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" @@ -7579,6 +7653,13 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nested@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" + integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== + dependencies: + postcss-selector-parser "^6.0.6" + postcss-normalize-charset@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" @@ -7677,6 +7758,14 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.7: + version "6.0.8" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz#f023ed7a9ea736cd7ef70342996e8e78645a7914" + integrity sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-svgo@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" @@ -7706,7 +7795,7 @@ postcss@^7.0.36: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.15, postcss@^8.2.6, postcss@^8.3.11, postcss@^8.3.5: +postcss@^8, postcss@^8.1.6, postcss@^8.2.15, postcss@^8.2.6, postcss@^8.3.11, postcss@^8.3.5: version "8.4.5" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== @@ -7971,6 +8060,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -8953,6 +9047,33 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" +tailwindcss@^3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.7.tgz#15936881f042a7eb8d6f2b6a454bac9f51181bbd" + integrity sha512-rZdKNHtC64jcQncLoWOuCzj4lQDTAgLtgK3WmQS88tTdpHh9OwLqULTQxI3tw9AMJsqSpCKlmcjW/8CSnni6zQ== + dependencies: + arg "^5.0.1" + chalk "^4.1.2" + chokidar "^3.5.2" + color-name "^1.1.4" + cosmiconfig "^7.0.1" + detective "^5.2.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.7" + glob-parent "^6.0.2" + is-glob "^4.0.3" + normalize-path "^3.0.0" + object-hash "^2.2.0" + postcss-js "^3.0.3" + postcss-load-config "^3.1.0" + postcss-nested "5.0.6" + postcss-selector-parser "^6.0.7" + postcss-value-parser "^4.2.0" + quick-lru "^5.1.1" + resolve "^1.20.0" + tmp "^0.2.1" + tapable@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -9072,6 +9193,13 @@ tippy.js@^6.2.3, tippy.js@^6.3.7: dependencies: "@popperjs/core" "^2.9.0" +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -9418,6 +9546,11 @@ vue-class-component@^7.2.3: resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4" integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w== +vue-cli-plugin-tailwind@^3.0.0-beta.0: + version "3.0.0-beta.0" + resolved "https://registry.yarnpkg.com/vue-cli-plugin-tailwind/-/vue-cli-plugin-tailwind-3.0.0-beta.0.tgz#274cd6e8638897a7661aa8e34db75ae66040fe92" + integrity sha512-JxuhnSgMcZgNZMkeb+hL30rxEphcGZqO+R0cqHjFMlSaD1fFIn/C9V4Ry3MBk3Y/mq7zZvYEyzXOYPc/jOd+uQ== + vue-eslint-parser@^8.0.0, vue-eslint-parser@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz#25e08b20a414551531f3e19f999902e1ecf45f13" @@ -10074,6 +10207,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" From e717312de71c32216a4fdf95ddcd07e0d84d0fac Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 28 Dec 2021 11:42:08 +0100 Subject: [PATCH 03/16] Introduce instances admin page Signed-off-by: Thomas Citharel --- config/config.exs | 1 + js/src/apollo/utils.ts | 3 + js/src/assets/logo.svg | 9 + js/src/components/Admin/Followers.vue | 262 --------------- js/src/components/Admin/Followings.vue | 311 ------------------ js/src/components/Settings/SettingsMenu.vue | 2 +- js/src/graphql/admin.ts | 61 ++++ js/src/i18n/en_US.json | 23 +- js/src/i18n/fr_FR.json | 23 +- js/src/mixins/relay.ts | 38 --- js/src/router/settings.ts | 64 ++-- js/src/types/enums.ts | 12 + js/src/types/instance.model.ts | 14 + js/src/views/Admin/AdminProfile.vue | 6 + js/src/views/Admin/Follows.vue | 116 ------- js/src/views/Admin/Instance.vue | 268 +++++++++++++++ js/src/views/Admin/Instances.vue | 305 +++++++++++++++++ lib/graphql/resolvers/admin.ex | 86 ++++- lib/graphql/schema/admin.ex | 124 +++++++ lib/mobilizon/actors/actors.ex | 10 + lib/mobilizon/instances/instance.ex | 19 ++ lib/mobilizon/instances/instances.ex | 115 +++++++ lib/service/workers/refresh_instances.ex | 31 ++ ...3141104_add_instance_materialized_view.exs | 64 ++++ schema.graphql | 226 ++++++++++++- 25 files changed, 1415 insertions(+), 778 deletions(-) create mode 100644 js/src/assets/logo.svg delete mode 100644 js/src/components/Admin/Followers.vue delete mode 100644 js/src/components/Admin/Followings.vue delete mode 100644 js/src/mixins/relay.ts create mode 100644 js/src/types/instance.model.ts delete mode 100644 js/src/views/Admin/Follows.vue create mode 100644 js/src/views/Admin/Instance.vue create mode 100644 js/src/views/Admin/Instances.vue create mode 100644 lib/mobilizon/instances/instance.ex create mode 100644 lib/mobilizon/instances/instances.ex create mode 100644 lib/service/workers/refresh_instances.ex create mode 100644 priv/repo/migrations/20211223141104_add_instance_materialized_view.exs diff --git a/config/config.exs b/config/config.exs index 73c3ae5d..10a20f43 100644 --- a/config/config.exs +++ b/config/config.exs @@ -290,6 +290,7 @@ config :mobilizon, Oban, crontab: [ {"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background}, {"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background}, + {"36 * * * *", Mobilizon.Service.Workers.RefreshInstances, queue: :background}, {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background}, diff --git a/js/src/apollo/utils.ts b/js/src/apollo/utils.ts index 84ba969e..07caeb40 100644 --- a/js/src/apollo/utils.ts +++ b/js/src/apollo/utils.ts @@ -70,6 +70,9 @@ export const typePolicies: TypePolicies = { participantStats: { merge: replaceMergePolicy }, }, }, + Instance: { + keyFields: ["domain"], + }, RootQueryType: { fields: { relayFollowers: paginatedLimitPagination(), diff --git a/js/src/assets/logo.svg b/js/src/assets/logo.svg new file mode 100644 index 00000000..32bac7d3 --- /dev/null +++ b/js/src/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/js/src/components/Admin/Followers.vue b/js/src/components/Admin/Followers.vue deleted file mode 100644 index d05b6d60..00000000 --- a/js/src/components/Admin/Followers.vue +++ /dev/null @@ -1,262 +0,0 @@ - - diff --git a/js/src/components/Admin/Followings.vue b/js/src/components/Admin/Followings.vue deleted file mode 100644 index 6739aba0..00000000 --- a/js/src/components/Admin/Followings.vue +++ /dev/null @@ -1,311 +0,0 @@ - - diff --git a/js/src/components/Settings/SettingsMenu.vue b/js/src/components/Settings/SettingsMenu.vue index bbf96cdc..3be7a96a 100644 --- a/js/src/components/Settings/SettingsMenu.vue +++ b/js/src/components/Settings/SettingsMenu.vue @@ -78,7 +78,7 @@ /> diff --git a/js/src/graphql/admin.ts b/js/src/graphql/admin.ts index acb3f73b..733e71fe 100644 --- a/js/src/graphql/admin.ts +++ b/js/src/graphql/admin.ts @@ -70,6 +70,67 @@ export const RELAY_FOLLOWINGS = gql` ${RELAY_FRAGMENT} `; +export const INSTANCE_FRAGMENT = gql` + fragment InstanceFragment on Instance { + domain + hasRelay + followerStatus + followedStatus + eventCount + personCount + groupCount + followersCount + followingsCount + reportsCount + mediaSize + } +`; + +export const INSTANCE = gql` + query instance($domain: ID!) { + instance(domain: $domain) { + ...InstanceFragment + } + } + ${INSTANCE_FRAGMENT} +`; + +export const INSTANCES = gql` + query Instances( + $page: Int + $limit: Int + $orderBy: InstancesSortFields + $direction: String + $filterDomain: String + $filterFollowStatus: InstanceFilterFollowStatus + $filterSuspendStatus: InstanceFilterSuspendStatus + ) { + instances( + page: $page + limit: $limit + orderBy: $orderBy + direction: $direction + filterDomain: $filterDomain + filterFollowStatus: $filterFollowStatus + filterSuspendStatus: $filterSuspendStatus + ) { + total + elements { + ...InstanceFragment + } + } + } + ${INSTANCE_FRAGMENT} +`; +export const ADD_INSTANCE = gql` + mutation addInstance($domain: String!) { + addInstance(domain: $domain) { + ...InstanceFragment + } + } + ${INSTANCE_FRAGMENT} +`; + export const ADD_RELAY = gql` mutation addRelay($address: String!) { addRelay(address: $address) { diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index d6ead134..bf831b87 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1260,5 +1260,24 @@ "This profile was not found": "This profile was not found", "Back to profile list": "Back to profile list", "This user was not found": "This user was not found", - "Back to user list": "Back to user list" -} + "Back to user list": "Back to user list", + "Stop following instance": "Stop following instance", + "Follow instance": "Follow instance", + "Accept follow": "Accept follow", + "Reject follow": "Reject follow", + "This instance doesn't follow yours.": "This instance doesn't follow yours.", + "Only Mobilizon instances can be followed": "", + "Follow a new instance": "Follow a new instance", + "Follow status": "Follow status", + "All": "All", + "Following": "Following", + "Followed": "Followed", + "Followed, pending response": "Followed, pending response", + "Follows us": "Follows us", + "Follows us, pending approval": "Follows us, pending approval", + "No instance found.": "No instance found.", + "No instances match this filter. Try resetting filter fields?": "No instances match this filter. Try resetting filter fields?", + "You haven't interacted with other instances yet.": "You haven't interacted with other instances yet.", + "mobilizon-instance.tld": "mobilizon-instance.tld", + "Report status": "Report status" +} \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index ff8bb112..77333a1a 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1260,5 +1260,24 @@ "{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})", "{title} ({count} todos)": "{title} ({count} todos)", "{username} was invited to {group}": "{username} a été invité à {group}", - "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" -} + "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", + "Stop following instance": "Arrêter de suivre l'instance", + "Follow instance": "Suivre l'instance", + "Accept follow": "Accepter le suivi", + "Reject follow": "Rejetter le suivi", + "This instance doesn't follow yours.": "Cette instance ne suit pas la vôtre.", + "Only Mobilizon instances can be followed": "Seules les instances Mobilizon peuvent être suivies", + "Follow a new instance": "Suivre une nouvelle instance", + "Follow status": "Statut du suivi", + "All": "Toutes", + "Following": "Suivantes", + "Followed": "Suivies", + "Followed, pending response": "Suivie, en attente de la réponse", + "Follows us": "Nous suit", + "Follows us, pending approval": "Nous suit, en attente de validation", + "No instance found": "Aucune instance trouvée", + "No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?", + "You haven't interacted with other instances yet.": "Vous n'avez interagi avec encore aucune autre instance.", + "mobilizon-instance.tld": "instance-mobilizon.tld", + "Report status": "Statut du signalement" +} \ No newline at end of file diff --git a/js/src/mixins/relay.ts b/js/src/mixins/relay.ts deleted file mode 100644 index 0289c139..00000000 --- a/js/src/mixins/relay.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IActor } from "@/types/actor"; -import { ActorType } from "@/types/enums"; -import { Component, Vue, Ref } from "vue-property-decorator"; -import VueRouter from "vue-router"; -const { isNavigationFailure, NavigationFailureType } = VueRouter; - -@Component -export default class RelayMixin extends Vue { - @Ref("table") readonly table!: any; - - toggle(row: Record): void { - this.table.toggleDetails(row); - } - - protected async pushRouter( - routeName: string, - args: Record - ): Promise { - try { - await this.$router.push({ - name: routeName, - query: { ...this.$route.query, ...args }, - }); - } catch (e) { - if (isNavigationFailure(e, NavigationFailureType.redirected)) { - throw Error(e.toString()); - } - } - } - - static isInstance(actor: IActor): boolean { - return ( - actor.type === ActorType.APPLICATION && - (actor.preferredUsername === "relay" || - actor.preferredUsername === actor.domain) - ); - } -} diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index 29b3de00..fcd8eeca 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -11,9 +11,8 @@ export enum SettingsRouteName { ADMIN = "ADMIN", ADMIN_DASHBOARD = "ADMIN_DASHBOARD", ADMIN_SETTINGS = "ADMIN_SETTINGS", - RELAYS = "Relays", - RELAY_FOLLOWINGS = "Followings", - RELAY_FOLLOWERS = "Followers", + INSTANCES = "INSTANCES", + INSTANCE = "INSTANCE", USERS = "USERS", PROFILES = "PROFILES", ADMIN_PROFILE = "ADMIN_PROFILE", @@ -199,44 +198,35 @@ export const settingsRoutes: RouteConfig[] = [ meta: { requiredAuth: true, announcer: { skip: true } }, }, { - path: "admin/relays", - name: SettingsRouteName.RELAYS, - redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS }, + path: "admin/instances", + name: SettingsRouteName.INSTANCES, component: (): Promise => - import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"), - meta: { requiredAuth: true, announcer: { skip: true } }, - children: [ - { - path: "followings", - name: SettingsRouteName.RELAY_FOLLOWINGS, - component: (): Promise => - import( - /* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue" - ), - meta: { - requiredAuth: true, - announcer: { - message: (): string => i18n.t("Followings") as string, - }, - }, + import( + /* webpackChunkName: "Instances" */ "@/views/Admin/Instances.vue" + ), + meta: { + requiredAuth: true, + announcer: { + message: (): string => i18n.t("Instances") as string, }, - { - path: "followers", - name: SettingsRouteName.RELAY_FOLLOWERS, - component: (): Promise => - import( - /* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue" - ), - meta: { - requiredAuth: true, - announcer: { - message: (): string => i18n.t("Followers") as string, - }, - }, - }, - ], + }, props: true, }, + { + path: "admin/instances/:domain", + name: SettingsRouteName.INSTANCE, + component: (): Promise => + import( + /* webpackChunkName: "Instance" */ "@/views/Admin/Instance.vue" + ), + props: true, + meta: { + requiredAuth: true, + announcer: { + message: (): string => i18n.t("Instance") as string, + }, + }, + }, { path: "/moderation", name: SettingsRouteName.MODERATION, diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts index 34049388..2f05bd0d 100644 --- a/js/src/types/enums.ts +++ b/js/src/types/enums.ts @@ -276,3 +276,15 @@ export enum EventMetadataCategories { BOOKING = "BOOKING", VIDEO_CONFERENCE = "VIDEO_CONFERENCE", } + +export enum InstanceFilterFollowStatus { + ALL = "ALL", + FOLLOWING = "FOLLOWING", + FOLLOWED = "FOLLOWED", +} + +export enum InstanceFollowStatus { + APPROVED = "APPROVED", + PENDING = "PENDING", + NONE = "NONE", +} diff --git a/js/src/types/instance.model.ts b/js/src/types/instance.model.ts new file mode 100644 index 00000000..5936f6ba --- /dev/null +++ b/js/src/types/instance.model.ts @@ -0,0 +1,14 @@ +import { InstanceFollowStatus } from "./enums"; + +export interface IInstance { + domain: string; + hasRelay: boolean; + followerStatus: InstanceFollowStatus; + followedStatus: InstanceFollowStatus; + personCount: number; + groupCount: number; + followersCount: number; + followingsCount: number; + reportsCount: number; + mediaSize: number; +} diff --git a/js/src/views/Admin/AdminProfile.vue b/js/src/views/Admin/AdminProfile.vue index 77040c16..32bb836e 100644 --- a/js/src/views/Admin/AdminProfile.vue +++ b/js/src/views/Admin/AdminProfile.vue @@ -384,6 +384,12 @@ export default class AdminProfile extends Vue { { key: this.$t("Domain") as string, value: this.person.domain ? this.person.domain : this.$t("Local"), + link: this.person.domain + ? { + name: RouteName.INSTANCE, + params: { domain: this.person.domain }, + } + : undefined, }, { key: this.$i18n.t("Uploaded media size"), diff --git a/js/src/views/Admin/Follows.vue b/js/src/views/Admin/Follows.vue deleted file mode 100644 index aae1acec..00000000 --- a/js/src/views/Admin/Follows.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - diff --git a/js/src/views/Admin/Instance.vue b/js/src/views/Admin/Instance.vue new file mode 100644 index 00000000..85233234 --- /dev/null +++ b/js/src/views/Admin/Instance.vue @@ -0,0 +1,268 @@ + + diff --git a/js/src/views/Admin/Instances.vue b/js/src/views/Admin/Instances.vue new file mode 100644 index 00000000..e2c64940 --- /dev/null +++ b/js/src/views/Admin/Instances.vue @@ -0,0 +1,305 @@ + + + + diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index 36857e7d..54c407ec 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do import Mobilizon.Users.Guards - alias Mobilizon.{Actors, Admin, Config, Events} + alias Mobilizon.{Actors, Admin, Config, Events, Instances} alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Cldr.Language @@ -329,6 +329,79 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:error, :unauthenticated} end + def get_instances( + _parent, + args, + %{ + context: %{current_user: %User{role: role}} + } + ) + when is_admin(role) do + {:ok, + Instances.instances( + args + |> Keyword.new() + |> Keyword.take([ + :page, + :limit, + :order_by, + :direction, + :filter_domain, + :filter_follow_status, + :filter_suspend_status + ]) + )} + end + + def get_instances(_parent, _args, %{context: %{current_user: %User{}}}) do + {:error, :unauthorized} + end + + def get_instances(_parent, _args, _resolution) do + {:error, :unauthenticated} + end + + def get_instance(_parent, %{domain: domain}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + has_relay = Actors.has_relay?(domain) + remote_relay = Actors.get_actor_by_name("relay@#{domain}") + local_relay = Relay.get_actor() + + result = %{ + has_relay: has_relay, + follower_status: follow_status(remote_relay, local_relay), + followed_status: follow_status(local_relay, remote_relay) + } + + {:ok, Map.merge(Instances.instance(domain), result)} + end + + def get_instance(_parent, _args, %{context: %{current_user: %User{}}}) do + {:error, :unauthorized} + end + + def get_instance(_parent, _args, _resolution) do + {:error, :unauthenticated} + end + + def create_instance( + parent, + %{domain: domain} = args, + %{context: %{current_user: %User{role: role}}} = resolution + ) + when is_admin(role) do + case Relay.follow(domain) do + {:ok, _activity, _follow} -> + Instances.refresh() + get_instance(parent, args, resolution) + + {:error, err} -> + {:error, err} + end + end + @spec create_relay(any(), map(), Absinthe.Resolution.t()) :: {:ok, Follower.t()} | {:error, any()} def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) @@ -425,4 +498,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do :ok end end + + @spec follow_status(Actor.t() | nil, Actor.t() | nil) :: :approved | :pending | :none + defp follow_status(follower, followed) when follower != nil and followed != nil do + case Actors.check_follow(follower, followed) do + %Follower{approved: true} -> :approved + %Follower{approved: false} -> :pending + _ -> :none + end + end + + defp follow_status(_, _), do: :none end diff --git a/lib/graphql/schema/admin.ex b/lib/graphql/schema/admin.ex index dfe02c09..44b70274 100644 --- a/lib/graphql/schema/admin.ex +++ b/lib/graphql/schema/admin.ex @@ -153,6 +153,80 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do value(:custom, as: "CUSTOM", description: "Custom privacy policy text") end + enum :instance_follow_status do + value(:approved, description: "The instance follow was approved") + value(:pending, description: "The instance follow is still pending") + value(:none, description: "There's no instance follow etablished") + end + + enum :instances_sort_fields do + value(:event_count) + value(:person_count) + value(:group_count) + value(:followers_count) + value(:followings_count) + value(:reports_count) + value(:media_size) + end + + enum :instance_filter_follow_status do + value(:all) + value(:following) + value(:followed) + end + + enum :instance_filter_suspend_status do + value(:all) + value(:suspended) + end + + @desc """ + An instance representation + """ + object :instance do + field(:domain, :id, description: "The domain name of the instance") + field(:has_relay, :boolean, description: "Whether this instance has a Mobilizon relay actor") + field(:follower_status, :instance_follow_status, description: "Do we follow this instance") + field(:followed_status, :instance_follow_status, description: "Does this instance follow us?") + + field(:event_count, :integer, description: "The number of events on this instance we know of") + + field(:person_count, :integer, + description: "The number of profiles on this instance we know of" + ) + + field(:group_count, :integer, description: "The number of grouo on this instance we know of") + + field(:followers_count, :integer, + description: "The number of their profiles who follow our groups" + ) + + field(:followings_count, :integer, + description: "The number of our profiles who follow their groups" + ) + + field(:reports_count, :integer, + description: "The number of reports made against profiles from this instance" + ) + + field(:media_size, :integer, + description: "The size of all the media files sent by actors from this instance" + ) + + field(:has_relay, :boolean, + description: + "Whether this instance has a relay, meaning that it's a Mobilizon instance that we can follow" + ) + end + + @desc """ + A paginated list of instances + """ + object :paginated_instance_list do + field(:elements, list_of(:instance), description: "A list of instances") + field(:total, :integer, description: "The total number of instances in the list") + end + object :admin_queries do @desc "Get the list of action logs" field :action_logs, type: :paginated_action_log_list do @@ -226,9 +300,59 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do arg(:direction, :string, default_value: :desc, description: "The sorting direction") resolve(&Admin.list_relay_followings/3) end + + @desc """ + List instances + """ + field :instances, :paginated_instance_list do + arg(:page, :integer, + default_value: 1, + description: "The page in the paginated relay followings list" + ) + + arg(:limit, :integer, + default_value: 10, + description: "The limit of relay followings per page" + ) + + arg(:order_by, :instances_sort_fields, + default_value: :event_count, + description: "The field to order by the list" + ) + + arg(:filter_domain, :string, default_value: nil, description: "Filter by domain") + + arg(:filter_follow_status, :instance_filter_follow_status, + default_value: :all, + description: "Whether or not to filter instances by the follow status" + ) + + arg(:filter_suspend_status, :instance_filter_suspend_status, + default_value: :all, + description: "Whether or not to filter instances by the suspended status" + ) + + arg(:direction, :string, default_value: :desc, description: "The sorting direction") + resolve(&Admin.get_instances/3) + end + + @desc """ + Get an instance's details + """ + field :instance, :instance do + arg(:domain, non_null(:id), description: "The instance domain") + resolve(&Admin.get_instance/3) + end end object :admin_mutations do + @desc "Add an instance subscription" + field :add_instance, type: :instance do + arg(:domain, non_null(:string), description: "The instance domain to add") + + resolve(&Admin.create_instance/3) + end + @desc "Add a relay subscription" field :add_relay, type: :follower do arg(:address, non_null(:string), description: "The relay hostname to add") diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 527aa5ee..b67b85e1 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1256,6 +1256,16 @@ defmodule Mobilizon.Actors do :ok end + @spec has_relay?(String.t()) :: boolean() + def has_relay?(domain) do + Actor + |> where( + [a], + a.preferred_username == "relay" and a.domain == ^domain and a.type == :Application + ) + |> Repo.exists?() + end + @spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do Enum.each([:avatar, :banner], fn key -> diff --git a/lib/mobilizon/instances/instance.ex b/lib/mobilizon/instances/instance.ex new file mode 100644 index 00000000..a3acedde --- /dev/null +++ b/lib/mobilizon/instances/instance.ex @@ -0,0 +1,19 @@ +defmodule Mobilizon.Instances.Instance do + @moduledoc """ + An instance representation + + Using a MATERIALIZED VIEW underneath + """ + use Ecto.Schema + + @primary_key {:domain, :string, []} + schema "instances" do + field(:event_count, :integer) + field(:person_count, :integer) + field(:group_count, :integer) + field(:followers_count, :integer) + field(:followings_count, :integer) + field(:reports_count, :integer) + field(:media_size, :integer) + end +end diff --git a/lib/mobilizon/instances/instances.ex b/lib/mobilizon/instances/instances.ex new file mode 100644 index 00000000..470e06c1 --- /dev/null +++ b/lib/mobilizon/instances/instances.ex @@ -0,0 +1,115 @@ +defmodule Mobilizon.Instances do + @moduledoc """ + The instances context + """ + alias Ecto.Adapters.SQL + alias Mobilizon.Actors.{Actor, Follower} + alias Mobilizon.Instances.Instance + alias Mobilizon.Storage.{Page, Repo} + import Ecto.Query + + @is_null_fragment "CASE WHEN ? IS NULL THEN FALSE ELSE TRUE END" + + @spec instances(Keyword.t()) :: Page.t(Instance.t()) + def instances(options) do + page = Keyword.get(options, :page) + limit = Keyword.get(options, :limit) + order_by = Keyword.get(options, :order_by) + direction = Keyword.get(options, :direction) + filter_domain = Keyword.get(options, :filter_domain) + # suspend_status = Keyword.get(options, :filter_suspend_status) + follow_status = Keyword.get(options, :filter_follow_status) + + order_by_options = Keyword.new([{direction, order_by}]) + + subquery = + Actor + |> where( + [a], + a.preferred_username == "relay" and a.type == :Application and not is_nil(a.domain) + ) + |> join(:left, [a], f1 in Follower, on: f1.target_actor_id == a.id) + |> join(:left, [a], f2 in Follower, on: f2.actor_id == a.id) + |> select([a, f1, f2], %{ + domain: a.domain, + has_relay: fragment(@is_null_fragment, a.id), + following: fragment(@is_null_fragment, f2.id), + following_approved: f2.approved, + follower: fragment(@is_null_fragment, f1.id), + follower_approved: f1.approved + }) + + query = + Instance + |> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain) + |> select([i, s], {i, s}) + |> order_by(^order_by_options) + + query = + if is_nil(filter_domain) or filter_domain == "" do + query + else + where(query, [i], like(i.domain, ^"%#{filter_domain}%")) + end + + query = + case follow_status do + :following -> where(query, [i, s], s.following == true) + :followed -> where(query, [i, s], s.follower == true) + :all -> query + end + + %Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain) + + %Page{ + paged_instances + | elements: Enum.map(elements, &convert_instance_meta/1) + } + end + + @spec instance(String.t()) :: Instance.t() + def instance(domain) do + Instance + |> where(domain: ^domain) + |> Repo.one() + end + + @spec all_domains :: list(Instance.t()) + def all_domains do + Instance + |> distinct(true) + |> select([:domain]) + |> Repo.all() + end + + @spec refresh :: %{ + :rows => nil | [[term()] | binary()], + :num_rows => non_neg_integer(), + optional(atom()) => any() + } + def refresh do + SQL.query!(Repo, "REFRESH MATERIALIZED VIEW instances") + end + + defp convert_instance_meta( + {instance, + %{ + domain: _domain, + follower: follower, + follower_approved: follower_approved, + following: following, + following_approved: following_approved, + has_relay: has_relay + }} + ) do + instance + |> Map.put(:follower_status, follow_status(following, following_approved)) + |> Map.put(:followed_status, follow_status(follower, follower_approved)) + |> Map.put(:has_relay, has_relay) + end + + defp follow_status(true, true), do: :approved + defp follow_status(true, false), do: :pending + defp follow_status(false, _), do: :none + defp follow_status(nil, _), do: :none +end diff --git a/lib/service/workers/refresh_instances.ex b/lib/service/workers/refresh_instances.ex new file mode 100644 index 00000000..23515b3f --- /dev/null +++ b/lib/service/workers/refresh_instances.ex @@ -0,0 +1,31 @@ +defmodule Mobilizon.Service.Workers.RefreshInstances do + @moduledoc """ + Worker to refresh the instances materialized view and the relay actors + """ + + use Oban.Worker, unique: [period: :infinity, keys: [:event_uuid, :action]] + + alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor + alias Mobilizon.Instances + alias Mobilizon.Instances.Instance + alias Oban.Job + + @impl Oban.Worker + @spec perform(Oban.Job.t()) :: :ok + def perform(%Job{}) do + Instances.refresh() + + Instances.all_domains() + |> Enum.each(&refresh_instance_actor/1) + end + + @spec refresh_instance_actor(Instance.t()) :: + {:ok, Mobilizon.Actors.Actor.t()} + | {:error, + Mobilizon.Federation.ActivityPub.Actor.make_actor_errors() + | Mobilizon.Federation.WebFinger.finger_errors()} + + defp refresh_instance_actor(%Instance{domain: domain}) do + ActivityPubActor.find_or_make_actor_from_nickname("relay@#{domain}") + end +end diff --git a/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs b/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs new file mode 100644 index 00000000..a997321c --- /dev/null +++ b/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs @@ -0,0 +1,64 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddInstanceMaterializedView do + use Ecto.Migration + + def up do + execute(""" + CREATE MATERIALIZED VIEW instances AS + SELECT + a.domain, + COUNT(DISTINCT(p.id)) AS person_count, + COUNT(DISTINCT(g.id)) AS group_count, + COUNT(DISTINCT(e.id)) AS event_count, + COUNT(f1.id) AS followers_count, + COUNT(f2.id) AS followings_count, + COUNT(r.id) AS reports_count, + SUM(COALESCE((m.file->>'size')::int, 0)) AS media_size + FROM actors a + LEFT JOIN actors p ON a.id = p.id AND p.type = 'Person' + LEFT JOIN actors g ON a.id = g.id AND g.type = 'Group' + LEFT JOIN events e ON a.id = e.organizer_actor_id + LEFT JOIN followers f1 ON a.id = f1.actor_id + LEFT JOIN followers f2 ON a.id = f2.target_actor_id + LEFT JOIN reports r ON r.reported_id = a.id + LEFT JOIN medias m ON m.actor_id = a.id + WHERE a.domain IS NOT NULL + GROUP BY a.domain; + """) + + execute(""" + CREATE OR REPLACE FUNCTION refresh_instances() + RETURNS trigger AS $$ + BEGIN + REFRESH MATERIALIZED VIEW instances; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + """) + + execute(""" + DROP TRIGGER IF EXISTS refresh_instances_trigger ON actors; + """) + + execute(""" + CREATE TRIGGER refresh_instances_trigger + AFTER INSERT OR UPDATE OR DELETE + ON actors + FOR EACH STATEMENT + EXECUTE PROCEDURE refresh_instances(); + """) + + create_if_not_exists(unique_index("instances", [:domain])) + end + + def down do + drop_if_exists(unique_index("instances", [:domain])) + + execute(""" + DROP FUNCTION IF EXISTS refresh_instances() CASCADE; + """) + + execute(""" + DROP MATERIALIZED VIEW IF EXISTS instances; + """) + end +end diff --git a/schema.graphql b/schema.graphql index fdb2a86a..70c11770 100644 --- a/schema.graphql +++ b/schema.graphql @@ -211,6 +211,9 @@ type Config { "The instance's features" features: Features + "The instance's restrictions" + restrictions: Restrictions + "The instance's version" version: String @@ -240,6 +243,9 @@ type Config { "Web Push settings for the instance" webPush: WebPush + + "The instance list of export formats" + exportFormats: ExportFormats } "A tag" @@ -306,7 +312,13 @@ type TodoList { actor: Actor "The todo-list's todos" - todos: PaginatedTodoList + todos( + "The page in the paginated todos list" + page: Int + + "The limit of todos per page" + limit: Int + ): PaginatedTodoList } "Represents a participant to an event" @@ -455,7 +467,7 @@ type Comment implements ActivityObject & ActionLogObject { isAnnouncement: Boolean! "The comment language" - language: String! + language: String } "An attached media or a link to a media" @@ -690,6 +702,9 @@ enum ExportFormatEnum { "PDF format" PDF + + "ODS format" + ODS } "The list of visibility options for a comment" @@ -770,6 +785,14 @@ interface Interactable { url: String } +enum EventType { + "The event will happen in person. It can also be livestreamed, but has a physical address" + IN_PERSON + + "The event will only happen online. It has no physical address" + ONLINE +} + enum EventMetadataType { "A string" STRING @@ -794,6 +817,14 @@ type DeletedObject { id: ID } +"A follow group event" +type FollowedGroupEvent { + user: User + profile: Person + group: Group + event: Event +} + "A paginated list of comments" type PaginatedCommentList { "A list of comments" @@ -851,7 +882,7 @@ type Post implements ActivityObject { updatedAt: DateTime "The post language" - language: String! + language: String "The post's tags" tags: [Tag] @@ -941,6 +972,12 @@ type Statistics { numberOfInstanceFollowings: Int } +"Export formats configuration" +type ExportFormats { + "The list of formats the event participants can be exported to" + eventParticipants: [String] +} + "Search persons result" type Persons { "Total elements" @@ -1128,6 +1165,18 @@ type Person implements ActionLogObject & Actor { "The limit of memberships per page" limit: Int ): PaginatedMemberList + + "The list of groups this person follows" + follows( + "Filter by group federated username" + group: String + + "The page in the follows list" + page: Int + + "The limit of follows per page" + limit: Int + ): PaginatedFollowerList } "Root Mutation" @@ -1396,6 +1445,30 @@ type RootMutationType { groupId: ID! ): DeletedObject + "Follow a group" + followGroup( + "The group ID" + groupId: ID! + + "Whether to notify profile from group activity" + notify: Boolean + ): Follower + + "Update a group follow" + updateGroupFollow( + "The follow ID" + followId: ID! + + "Whether to notify profile from group activity" + notify: Boolean + ): Follower + + "Unfollow a group" + unfollowGroup( + "The group ID" + groupId: ID! + ): Follower + "Create an event" createEvent( "The event's title" @@ -1589,6 +1662,9 @@ type RootMutationType { "The anonymous participant's locale" locale: String + + "The anonymous participant's timezone" + timezone: String ): Participant "Leave an event" @@ -1663,6 +1739,18 @@ type RootMutationType { id: ID! ): Member + "Approve a membership request" + approveMember( + "The member ID" + memberId: ID! + ): Member + + "Reject a membership request" + rejectMember( + "The member ID" + memberId: ID! + ): Member + "Update a member's role" updateMember( "The member ID" @@ -1674,11 +1762,11 @@ type RootMutationType { "Remove a member from a group" removeMember( - "The group ID" - groupId: ID! - "The member ID" memberId: ID! + + "Whether the member should be excluded from the group" + exclude: Boolean ): Member "Create a Feed Token" @@ -2108,6 +2196,12 @@ type RootQueryType { "A geohash for coordinates" location: String + "Whether to include the groups the current actor is member or follower" + excludeMyGroups: Boolean + + "The minimum visibility the group must have" + minimumVisibility: GroupVisibility + "Radius around the location to search in" radius: Float @@ -2128,6 +2222,9 @@ type RootQueryType { "A geohash for coordinates" location: String + "Whether the event is online or in person" + type: EventType + "Radius around the location to search in" radius: Float @@ -2389,6 +2486,12 @@ type RootQueryType { direction: String ): PaginatedFollowerList + "Get an instance's details" + instance( + "The instance domain" + domain: ID! + ): Instance + "Get a todo list" todoList( "The todo-list ID" @@ -2461,6 +2564,39 @@ string. """ scalar NaiveDateTime +"An instance representation" +type Instance { + "The domain name of the instance" + domain: ID + + "Whether this instance has a Mobilizon relay actor" + hasRelay: Boolean + + "Do we follow this instance" + followerStatus: InstanceFollowStatus + + "Does this instance follow us?" + followedStatus: InstanceFollowStatus + + "The number of profiles on this instance we know of" + personCount: Int + + "The number of grouo on this instance we know of" + groupCount: Int + + "The number of their profiles who follow our groups" + followersCount: Int + + "The number of our profiles who follow their groups" + followingsCount: Int + + "The number of reports made against profiles from this instance" + reportsCount: Int + + "The size of all the media files sent by actors from this instance" + mediaSize: Int +} + """ The `DateTime` scalar type represents a date and time in the UTC timezone. The DateTime appears in a JSON response as an ISO8601 formatted @@ -2573,8 +2709,14 @@ input EventOptionsInput { "Show event end time" showEndTime: Boolean + "The event's timezone" + timezone: String + "Whether to show or hide the person organizer when event is organized by a group" hideOrganizerWhenGroupEvent: Boolean + + "Whether the event is fully online" + isOnline: Boolean } "A report object" @@ -2765,7 +2907,7 @@ type Event implements ActivityObject & Interactable & ActionLogObject { metadata: [EventMetadata] "The event language" - language: String! + language: String } "An event offer" @@ -2835,6 +2977,18 @@ input AddressInput { "The address's original ID from the provider" originId: String + + "The (estimated) timezone of the location" + timezone: String +} + +"The instance's restrictions" +type Restrictions { + "Whether groups creation is allowed only for admin, not for all users" + onlyAdminCanCreateGroups: Boolean + + "Whether events creation is allowed only for groups, not for persons" + onlyGroupsCanCreateEvents: Boolean } "Instance anonymous configuration" @@ -2916,8 +3070,14 @@ type EventOptions { "Show event end time" showEndTime: Boolean + "The event's timezone" + timezone: String + "Whether to show or hide the person organizer when event is organized by a group" hideOrganizerWhenGroupEvent: Boolean + + "Whether the event is fully online" + isOnline: Boolean } "A resource provider details" @@ -2958,6 +3118,9 @@ type Follower { "Whether the follow has been approved by the target actor" approved: Boolean + "Whether the follower will be notified by the target actor's activity or not (applicable for profile\/group follows)" + notify: Boolean + "When the follow was created" insertedAt: DateTime @@ -3322,6 +3485,15 @@ type PaginatedGroupList { total: Int } +"A paginated list of follow group events" +type PaginatedFollowedGroupEvents { + "A list of follow group events" + elements: [FollowedGroupEvent] + + "The total number of follow group events in the list" + total: Int +} + "Instance map tiles configuration" type Tiles { "The instance's tiles endpoint" @@ -3377,6 +3549,20 @@ type Address { "The address's original ID from the provider" originId: String + + "The (estimated) timezone of the location" + timezone: String +} + +enum InstanceFollowStatus { + "The instance follow was approved" + APPROVED + + "The instance follow is still pending" + PENDING + + "There's no instance follow etablished" + NONE } "The instance's terms configuration" @@ -3599,6 +3785,9 @@ type User implements ActionLogObject { "The list of memberships for this user" memberships( + "A name to filter members by" + name: String + "The page in the paginated memberships list" page: Int @@ -3615,6 +3804,18 @@ type User implements ActionLogObject { limit: Int ): [Event] + "The suggested events from the groups this user follows" + followedGroupEvents( + "The page in the follow group events list" + page: Int + + "The limit of follow group events per page" + limit: Int + + "Filter follow group events by event start datetime" + afterDatetime: DateTime + ): PaginatedFollowedGroupEvents + "The list of settings for this user" settings: UserSettings @@ -3731,6 +3932,9 @@ type Group implements ActionLogObject & ActivityObject & Interactable & Actor { "A paginated list of group members" members( + "A name to filter members by" + name: String + "The page in the paginated member list" page: Int @@ -3760,7 +3964,13 @@ type Group implements ActionLogObject & ActivityObject & Interactable & Actor { ): PaginatedPostList "A paginated list of the todo lists this group has" - todoLists: PaginatedTodoListList + todoLists( + "The page in the paginated todo-lists list" + page: Int + + "The limit of todo-lists per page" + limit: Int + ): PaginatedTodoListList "A paginated list of the followers this group has" followers( From bc6cec45fa9f8cbdb74daa9278dd00248f3526ce Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 29 Dec 2021 14:58:38 +0100 Subject: [PATCH 04/16] Improve logging in module and handle more Webfinger errors Signed-off-by: Thomas Citharel --- lib/federation/web_finger/web_finger.ex | 30 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex index 9f1ae34e..7568a9b7 100644 --- a/lib/federation/web_finger/web_finger.ex +++ b/lib/federation/web_finger/web_finger.ex @@ -125,7 +125,11 @@ defmodule Mobilizon.Federation.WebFinger do defp maybe_add_profile_page(data, _actor), do: data @type finger_errors :: - :host_not_found | :address_invalid | :http_error | :webfinger_information_not_json + :host_not_found + | :address_invalid + | :http_error + | :webfinger_information_not_json + | :no_url_in_webfinger_data @doc """ Finger an actor to retreive it's ActivityPub ID/URL @@ -144,6 +148,10 @@ defmodule Mobilizon.Federation.WebFinger do {:ok, %{"url" => url}} -> {:ok, url} + {:ok, _} -> + Logger.debug("No URL found for actor from webfinger data") + {:error, :no_url_in_webfinger_data} + {:error, err} -> Logger.debug("Couldn't process webfinger data for #{actor}") {:error, err} @@ -158,11 +166,14 @@ defmodule Mobilizon.Federation.WebFinger do @spec fetch_webfinger_data(String.t()) :: {:ok, map()} | {:error, :webfinger_information_not_json | :http_error} defp fetch_webfinger_data(address) do + Logger.debug("Calling WebfingerClient with #{inspect(address)}") + case WebfingerClient.get(address) do {:ok, %{body: body, status: code}} when code in 200..299 -> webfinger_from_json(body) - _ -> + err -> + Logger.debug("Failed to fetch webfinger data #{inspect(err)}") {:error, :http_error} end end @@ -173,12 +184,14 @@ defmodule Mobilizon.Federation.WebFinger do case apply_webfinger_endpoint(actor) do address when is_binary(address) -> if address_invalid(address) do + Logger.info("Webfinger endpoint seems to be an invalid URL #{inspect(address)}") {:error, :address_invalid} else {:ok, address} end _ -> + Logger.info("Host not found in actor address #{inspect(actor)}") {:error, :host_not_found} end end @@ -188,12 +201,15 @@ defmodule Mobilizon.Federation.WebFinger do @spec find_webfinger_endpoint(String.t()) :: {:ok, String.t()} | {:error, :link_not_found} | {:error, any()} defp find_webfinger_endpoint(domain) when is_binary(domain) do + Logger.debug("Calling HostMetaClient for #{domain}") + with {:ok, %Tesla.Env{status: 200, body: body}} <- - HostMetaClient.get("http://#{domain}/.well-known/host-meta"), + HostMetaClient.get("https://#{domain}/.well-known/host-meta"), link_template when is_binary(link_template) <- find_link_from_template(body) do {:ok, link_template} else {:ok, %Tesla.Env{status: 404}} -> {:error, :entity_not_found} + {:ok, %Tesla.Env{}} -> {:error, :http_error} {:error, :link_not_found} -> {:error, :link_not_found} {:error, error} -> {:error, error} end @@ -204,10 +220,12 @@ defmodule Mobilizon.Federation.WebFinger do with {:ok, domain} <- domain_from_federated_actor(actor) do case find_webfinger_endpoint(domain) do {:ok, link_template} -> + Logger.debug("Using webfinger location provided by host-meta endpoint") String.replace(link_template, "{uri}", "acct:#{actor}") _ -> - "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}" + Logger.debug("Using default webfinger location") + "https://#{domain}/.well-known/webfinger?resource=acct:#{actor}" end end end @@ -233,6 +251,10 @@ defmodule Mobilizon.Federation.WebFinger do {"application/activity+json", "self"} -> Map.put(data, "url", link["href"]) + {nil, _rel} -> + Logger.debug("No type declared for the following link #{inspect(link)}") + data + _ -> Logger.debug(fn -> "Unhandled type to finger: #{inspect(link["type"])}" From 1319985047e7e07ba180448e547bde64faa9fba8 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 29 Dec 2021 14:59:33 +0100 Subject: [PATCH 05/16] Rename Mobilizon.Actors.is_following/2 to check_follow/2 Signed-off-by: Thomas Citharel --- lib/graphql/api/follows.ex | 4 +-- lib/mobilizon/actors/actors.ex | 8 +++--- .../transmogrifier/follow_test.exs | 28 +++++++++---------- .../activity_pub/transmogrifier/undo_test.exs | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index f30219fe..05c87b0b 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Follows do "We're trying to accept a follow: #{followed_url} is accepting #{follower_url} follow request." ) - case Actors.is_following(follower, followed) do + case Actors.check_follow(follower, followed) do %Follower{approved: false} = follow -> Actions.Accept.accept( :follow, @@ -68,7 +68,7 @@ defmodule Mobilizon.GraphQL.API.Follows do "We're trying to reject a follow: #{followed_url} is rejecting #{follower_url} follow request." ) - case Actors.is_following(follower, followed) do + case Actors.check_follow(follower, followed) do %Follower{approved: false} -> {:error, "Follow already rejected"} diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index b67b85e1..b264c4dc 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1176,7 +1176,7 @@ defmodule Mobilizon.Actors do if followed.suspended do {:error, :followed_suspended} else - case is_following(follower, followed) do + case check_follow(follower, followed) do %Follower{} -> {:error, :already_following} @@ -1202,7 +1202,7 @@ defmodule Mobilizon.Actors do @spec unfollow(Actor.t(), Actor.t()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t() | String.t()} def unfollow(%Actor{} = followed, %Actor{} = follower) do - case {:already_following, is_following(follower, followed)} do + case {:already_following, check_follow(follower, followed)} do {:already_following, %Follower{} = follow} -> delete_follower(follow) @@ -1214,8 +1214,8 @@ defmodule Mobilizon.Actors do @doc """ Checks whether an actor is following another actor. """ - @spec is_following(Actor.t(), Actor.t()) :: Follower.t() | nil - def is_following(%Actor{} = follower_actor, %Actor{} = followed_actor) do + @spec check_follow(Actor.t(), Actor.t()) :: Follower.t() | nil + def check_follow(%Actor{} = follower_actor, %Actor{} = followed_actor) do get_follower_by_followed_and_following(followed_actor, follower_actor) end diff --git a/test/federation/activity_pub/transmogrifier/follow_test.exs b/test/federation/activity_pub/transmogrifier/follow_test.exs index 285fea0c..925bdd82 100644 --- a/test/federation/activity_pub/transmogrifier/follow_test.exs +++ b/test/federation/activity_pub/transmogrifier/follow_test.exs @@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do end) =~ "Only group and instances can be followed" actor = Actors.get_actor_with_preload(actor.id) - refute Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor) + refute Actors.check_follow(Actors.get_actor_by_url!(data["actor"], true), actor) end test "it works for incoming follow requests" do @@ -68,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" actor = Actors.get_actor_with_preload(actor.id) - assert Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor) + assert Actors.check_follow(Actors.get_actor_by_url!(data["actor"], true), actor) end test "it rejects activities without a valid ID" do @@ -97,7 +97,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do # assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" # assert data["type"] == "Follow" # assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" - # assert User.is_following(User.get_by_ap_id(data["actor"]), user) + # assert User.check_follow(User.get_by_ap_id(data["actor"]), user) # end end @@ -106,10 +106,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do follower = insert(:actor) followed = insert(:group, manually_approves_followers: false) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) follow_object_id = follow_activity.data["id"] @@ -131,17 +131,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it works for incoming accepts which were pre-accepted" do follower = insert(:actor) followed = insert(:group, manually_approves_followers: true) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) follow_object_id = follow_activity.data["id"] @@ -166,7 +166,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it works for incoming accepts which are referenced by IRI only" do @@ -188,7 +188,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it fails for incoming accepts which cannot be correlated" do @@ -207,7 +207,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end end @@ -228,7 +228,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end test "it works for incoming rejects which are referenced by IRI only" do @@ -237,7 +237,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) reject_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -247,7 +247,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end end end diff --git a/test/federation/activity_pub/transmogrifier/undo_test.exs b/test/federation/activity_pub/transmogrifier/undo_test.exs index 74d9b30f..35632690 100644 --- a/test/federation/activity_pub/transmogrifier/undo_test.exs +++ b/test/federation/activity_pub/transmogrifier/undo_test.exs @@ -82,7 +82,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do assert data["actor"] == "https://social.tcit.fr/users/tcit" {:ok, followed} = Actors.get_actor_by_url(data["actor"]) - refute Actors.is_following(followed, actor) + refute Actors.check_follow(followed, actor) end end end From 26b1ea401ac782e9e239e4a56743e070f9b6192a Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 6 Jan 2022 18:48:48 +0100 Subject: [PATCH 06/16] Fix various issues reported by Dializer Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/actor.ex | 6 +++--- lib/federation/activity_pub/audience.ex | 2 +- lib/federation/activity_pub/refresher.ex | 2 +- lib/graphql/api/follows.ex | 3 ++- lib/mix/tasks/mobilizon/instance.ex | 2 +- lib/mix/tasks/mobilizon/users/new.ex | 4 ---- lib/mobilizon/actors/actors.ex | 2 +- lib/service/workers/clean_unconfirmed_users_worker.ex | 7 ++++++- lib/service/workers/helper.ex | 2 +- test/federation/activity_pub/actor_test.exs | 8 ++++---- test/federation/web_finger/web_finger_test.exs | 10 +++++----- test/graphql/resolvers/member_test.exs | 4 ++-- 12 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/federation/activity_pub/actor.ex b/lib/federation/activity_pub/actor.ex index 42bd31d8..8f4f07b4 100644 --- a/lib/federation/activity_pub/actor.ex +++ b/lib/federation/activity_pub/actor.ex @@ -108,15 +108,15 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do @doc """ Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it """ - @spec make_actor_from_nickname(nickname :: String.t(), preload :: boolean) :: + @spec make_actor_from_nickname(nickname :: String.t(), options :: Keyword.t()) :: {:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()} - def make_actor_from_nickname(nickname, preload \\ false) do + def make_actor_from_nickname(nickname, options \\ []) do Logger.debug("Fingering actor from nickname #{nickname}") case WebFinger.finger(nickname) do {:ok, url} when is_binary(url) -> Logger.debug("Matched #{nickname} to URL #{url}, now making actor") - make_actor_from_url(url, preload: preload) + make_actor_from_url(url, options) {:error, e} -> {:error, e} diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index 540ba76e..19502dfa 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -20,7 +20,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do @doc """ Get audience for an entity """ - @spec get_audience(Entity.t()) :: audience() + @spec get_audience(Entity.t() | Participant.t()) :: audience() def get_audience(%Event{} = event) do extract_actors_from_event(event) end diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex index bfa4f6c1..4a8fc8c6 100644 --- a/lib/federation/activity_pub/refresher.ex +++ b/lib/federation/activity_pub/refresher.ex @@ -190,7 +190,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do # If we're handling an activity @spec handling_element(map()) :: {:ok, any, struct} | :error - @spec handling_element(String.t()) :: {:ok, struct} | {:error, any()} + @spec handling_element(String.t()) :: {:ok, struct} | {:ok, atom, struct} | {:error, any()} defp handling_element(%{"type" => activity_type} = data) when activity_type in ["Create", "Update", "Delete"] do object = get_in(data, ["object"]) diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index 05c87b0b..d981c89b 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -69,7 +69,8 @@ defmodule Mobilizon.GraphQL.API.Follows do ) case Actors.check_follow(follower, followed) do - %Follower{approved: false} -> + %Follower{approved: false} = follow -> + Actors.delete_follower(follow) {:error, "Follow already rejected"} %Follower{} = follow -> diff --git a/lib/mix/tasks/mobilizon/instance.ex b/lib/mix/tasks/mobilizon/instance.ex index 3d3dccc6..180bb17b 100644 --- a/lib/mix/tasks/mobilizon/instance.ex +++ b/lib/mix/tasks/mobilizon/instance.ex @@ -130,7 +130,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do options, :listen_port, "What port will the app listen to (leave it if you are using the default setup with nginx)?", - 4000 + "4000" ) instance_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) diff --git a/lib/mix/tasks/mobilizon/users/new.ex b/lib/mix/tasks/mobilizon/users/new.ex index db013f7c..df39c596 100644 --- a/lib/mix/tasks/mobilizon/users/new.ex +++ b/lib/mix/tasks/mobilizon/users/new.ex @@ -67,10 +67,6 @@ defmodule Mix.Tasks.Mobilizon.Users.New do {:error, %Ecto.Changeset{errors: errors}} -> shell_error(inspect(errors)) shell_error("User has not been created because of the above reason.") - - err -> - shell_error(inspect(err)) - shell_error("User has not been created because of an unknown reason.") end end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index b264c4dc..1df299fb 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1475,7 +1475,7 @@ defmodule Mobilizon.Actors do |> where([_q, ..., a], like(a.name, ^"%#{name}%") or like(a.preferred_username, ^"%#{name}%")) end - @spec join_members_actor(Ecto.Query.t()) :: Ecto.Query.t() + @spec join_members_actor(Ecto.Queryable.t()) :: Ecto.Query.t() defp join_members_actor(query) do join(query, :inner, [q], a in Actor, on: q.actor_id == a.id) end diff --git a/lib/service/workers/clean_unconfirmed_users_worker.ex b/lib/service/workers/clean_unconfirmed_users_worker.ex index 73646beb..2df01b27 100644 --- a/lib/service/workers/clean_unconfirmed_users_worker.ex +++ b/lib/service/workers/clean_unconfirmed_users_worker.ex @@ -8,7 +8,12 @@ defmodule Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker do @impl Oban.Worker def perform(%Job{}) do - if Mobilizon.Config.get!([:instance, :remove_unconfirmed_users]) and should_perform?() do + remove_unconfirmed_users = + :mobilizon + |> Application.get_env(:instance) + |> Keyword.get(:remove_unconfirmed_users, false) + + if remove_unconfirmed_users and should_perform?() do CleanUnconfirmedUsers.clean() end end diff --git a/lib/service/workers/helper.ex b/lib/service/workers/helper.ex index 28bb0d7b..8cb14f39 100644 --- a/lib/service/workers/helper.ex +++ b/lib/service/workers/helper.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.Service.Workers.Helper do alias Oban.Job - @spec enqueue(String.t(), map(), Keyword.t()) :: + @spec enqueue(String.t() | :atom, map(), Keyword.t()) :: {:ok, Job.t()} | {:error, Ecto.Changeset.t()} def enqueue(operation, params, worker_args \\ []) do params = Map.merge(%{"op" => operation}, params) diff --git a/test/federation/activity_pub/actor_test.exs b/test/federation/activity_pub/actor_test.exs index 5298f0e8..d402822b 100644 --- a/test/federation/activity_pub/actor_test.exs +++ b/test/federation/activity_pub/actor_test.exs @@ -30,7 +30,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framapiaf.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -43,7 +43,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do |> expect(:call, fn %{ method: :get, - url: "http://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" + url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" }, _opts -> {:ok, %Tesla.Env{status: 200, body: webfinger_data}} @@ -69,7 +69,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framapiaf.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -82,7 +82,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do |> expect(:call, fn %{ method: :get, - url: "http://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" + url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" }, _opts -> {:ok, %Tesla.Env{status: 200, body: webfinger_data}} diff --git a/test/federation/web_finger/web_finger_test.exs b/test/federation/web_finger/web_finger_test.exs index c3cb48af..5cd5f2f2 100644 --- a/test/federation/web_finger/web_finger_test.exs +++ b/test/federation/web_finger/web_finger_test.exs @@ -72,7 +72,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://social.tcit.fr/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://social.tcit.fr/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -100,7 +100,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://pleroma.soykaf.com/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://pleroma.soykaf.com/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -127,7 +127,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framatube.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framatube.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -154,7 +154,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://squeet.me/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://squeet.me/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -182,7 +182,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://demo.gancio.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://demo.gancio.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) diff --git a/test/graphql/resolvers/member_test.exs b/test/graphql/resolvers/member_test.exs index 30e4a0cc..e2ed4990 100644 --- a/test/graphql/resolvers/member_test.exs +++ b/test/graphql/resolvers/member_test.exs @@ -301,7 +301,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://nowhere.absolute/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://nowhere.absolute/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -310,7 +310,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do %{ method: :get, url: - "http://nowhere.absolute/.well-known/webfinger?resource=acct:not_existing@nowhere.absolute" + "https://nowhere.absolute/.well-known/webfinger?resource=acct:not_existing@nowhere.absolute" }, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} From 193fcde123d461f0d0fdb4e7adb2481c2efdcc67 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 10 Jan 2022 10:17:50 +0100 Subject: [PATCH 07/16] Allow to filter reports by domain Signed-off-by: Thomas Citharel --- js/src/graphql/report.ts | 9 ++- js/src/router/settings.ts | 4 +- js/src/views/Admin/Instance.vue | 5 +- js/src/views/Moderation/ReportList.vue | 84 +++++++++++++++++--------- lib/graphql/resolvers/report.ex | 12 +++- lib/graphql/schema/report.ex | 1 + lib/mobilizon/reports/reports.ex | 38 +++++++----- 7 files changed, 104 insertions(+), 49 deletions(-) diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 3fcffe8e..d2543618 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -2,8 +2,13 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; export const REPORTS = gql` - query Reports($status: ReportStatus, $page: Int, $limit: Int) { - reports(status: $status, page: $page, limit: $limit) { + query Reports( + $status: ReportStatus + $domain: String + $page: Int + $limit: Int + ) { + reports(status: $status, domain: $domain, page: $page, limit: $limit) { total elements { id diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index fcd8eeca..c71730ba 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -20,7 +20,7 @@ export enum SettingsRouteName { ADMIN_GROUPS = "ADMIN_GROUPS", ADMIN_GROUP_PROFILE = "ADMIN_GROUP_PROFILE", MODERATION = "MODERATION", - REPORTS = "Reports", + REPORTS = "REPORTS", REPORT = "Report", REPORT_LOGS = "Logs", CREATE_IDENTITY = "CreateIdentity", @@ -234,7 +234,7 @@ export const settingsRoutes: RouteConfig[] = [ meta: { requiredAuth: true, announcer: { skip: true } }, }, { - path: "/moderation/reports/:filter?", + path: "/moderation/reports", name: SettingsRouteName.REPORTS, component: (): Promise => import( diff --git a/js/src/views/Admin/Instance.vue b/js/src/views/Admin/Instance.vue index 85233234..bfb4bd18 100644 --- a/js/src/views/Admin/Instance.vue +++ b/js/src/views/Admin/Instance.vue @@ -70,7 +70,10 @@ {{ $t("Followers") }}
- + {{ instance.reportsCount }} diff --git a/js/src/views/Moderation/ReportList.vue b/js/src/views/Moderation/ReportList.vue index bce76169..a1e278b9 100644 --- a/js/src/views/Moderation/ReportList.vue +++ b/js/src/views/Moderation/ReportList.vue @@ -15,23 +15,37 @@
- - {{ $t("Open") }} + + {{ $t("Open") }} + {{ $t("Resolved") }} + {{ $t("Closed") }} + + - {{ $t("Resolved") }} - {{ $t("Closed") }} - + + +
  • diff --git a/js/src/main.ts b/js/src/main.ts index 317f2b37..0e669519 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -12,6 +12,7 @@ import { NotifierPlugin } from "./plugins/notifier"; import filters from "./filters"; import { i18n } from "./utils/i18n"; import apolloProvider from "./vue-apollo"; +import Breadcrumbs from "@/components/Utils/Breadcrumbs.vue"; import "./registerServiceWorker"; import "./assets/tailwind.css"; @@ -25,6 +26,7 @@ Vue.use(VueScrollTo); Vue.use(VTooltip); Vue.use(VueAnnouncer); Vue.use(VueSkipTo); +Vue.component("breadcrumbs-nav", Breadcrumbs); // Register the router hooks with their names Component.registerHooks([ diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue index 0bba7acd..d0e56dd4 100644 --- a/js/src/views/Account/children/EditIdentity.vue +++ b/js/src/views/Account/children/EditIdentity.vue @@ -1,28 +1,6 @@