+
{{ $t("No resources in this folder") }}
@@ -470,12 +470,12 @@ export default class Resources extends Mixins(ResourceMixin) {
handleRename(resource: IResource) {
this.renameModal = true;
- this.updatedResource = Object.assign({}, resource);
+ this.updatedResource = { ...resource };
}
handleMove(resource: IResource) {
this.moveModal = true;
- this.updatedResource = Object.assign({}, resource);
+ this.updatedResource = { ...resource };
}
async moveResource(resource: IResource, oldParent: IResource | undefined) {
diff --git a/js/yarn.lock b/js/yarn.lock
index fb896df7..ce0a49a7 100644
--- a/js/yarn.lock
+++ b/js/yarn.lock
@@ -49,32 +49,32 @@
"@babel/highlight" "^7.10.4"
"@babel/compat-data@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.4.tgz#706a6484ee6f910b719b696a9194f8da7d7ac241"
- integrity sha512-t+rjExOrSVvjQQXNp5zAIYDp00KjdvGl/TpDX5REPr0S9IAIPQMTilcfG6q8c0QFmj9lSTVySV2VTsyggvtNIw==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.5.tgz#d38425e67ea96b1480a3f50404d1bf85676301a6"
+ integrity sha512-mPVoWNzIpYJHbWje0if7Ck36bpbtTvIxOi9+6WSK9wjGEXearAqlwBoTQvVjsAY2VIwgcs8V940geY3okzRCEw==
dependencies:
browserslist "^4.12.0"
invariant "^2.2.4"
semver "^5.5.0"
"@babel/core@^7.7.5", "@babel/core@^7.9.6":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.4.tgz#780e8b83e496152f8dd7df63892b2e052bf1d51d"
- integrity sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330"
+ integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w==
dependencies:
"@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.10.4"
- "@babel/helper-module-transforms" "^7.10.4"
+ "@babel/generator" "^7.10.5"
+ "@babel/helper-module-transforms" "^7.10.5"
"@babel/helpers" "^7.10.4"
- "@babel/parser" "^7.10.4"
+ "@babel/parser" "^7.10.5"
"@babel/template" "^7.10.4"
- "@babel/traverse" "^7.10.4"
- "@babel/types" "^7.10.4"
+ "@babel/traverse" "^7.10.5"
+ "@babel/types" "^7.10.5"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.1"
json5 "^2.1.2"
- lodash "^4.17.13"
+ lodash "^4.17.19"
resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.5.0"
@@ -90,14 +90,13 @@
source-map "^0.5.0"
trim-right "^1.0.1"
-"@babel/generator@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.4.tgz#e49eeed9fe114b62fa5b181856a43a5e32f5f243"
- integrity sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==
+"@babel/generator@^7.10.5":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69"
+ integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig==
dependencies:
- "@babel/types" "^7.10.4"
+ "@babel/types" "^7.10.5"
jsesc "^2.5.1"
- lodash "^4.17.13"
source-map "^0.5.0"
"@babel/helper-annotate-as-pure@^7.10.4":
@@ -126,13 +125,13 @@
levenary "^1.1.1"
semver "^5.5.0"
-"@babel/helper-create-class-features-plugin@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.4.tgz#2d4015d0136bd314103a70d84a7183e4b344a355"
- integrity sha512-9raUiOsXPxzzLjCXeosApJItoMnX3uyT4QdM2UldffuGApNrF8e938MwNpDCK9CPoyxrEoCgT+hObJc3mZa6lQ==
+"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d"
+ integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==
dependencies:
"@babel/helper-function-name" "^7.10.4"
- "@babel/helper-member-expression-to-functions" "^7.10.4"
+ "@babel/helper-member-expression-to-functions" "^7.10.5"
"@babel/helper-optimise-call-expression" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/helper-replace-supers" "^7.10.4"
@@ -148,13 +147,13 @@
regexpu-core "^4.7.0"
"@babel/helper-define-map@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.4.tgz#f037ad794264f729eda1889f4ee210b870999092"
- integrity sha512-nIij0oKErfCnLUCWaCaHW0Bmtl2RO9cN7+u2QT8yqTywgALKlyUVOvHDElh+b5DwVC6YB1FOYFOTWcN/+41EDA==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30"
+ integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==
dependencies:
"@babel/helper-function-name" "^7.10.4"
- "@babel/types" "^7.10.4"
- lodash "^4.17.13"
+ "@babel/types" "^7.10.5"
+ lodash "^4.17.19"
"@babel/helper-explode-assignable-expression@^7.10.4":
version "7.10.4"
@@ -187,12 +186,12 @@
dependencies:
"@babel/types" "^7.10.4"
-"@babel/helper-member-expression-to-functions@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz#7cd04b57dfcf82fce9aeae7d4e4452fa31b8c7c4"
- integrity sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A==
+"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee"
+ integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA==
dependencies:
- "@babel/types" "^7.10.4"
+ "@babel/types" "^7.10.5"
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3":
version "7.10.4"
@@ -201,18 +200,18 @@
dependencies:
"@babel/types" "^7.10.4"
-"@babel/helper-module-transforms@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d"
- integrity sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q==
+"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6"
+ integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA==
dependencies:
"@babel/helper-module-imports" "^7.10.4"
"@babel/helper-replace-supers" "^7.10.4"
"@babel/helper-simple-access" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.10.4"
"@babel/template" "^7.10.4"
- "@babel/types" "^7.10.4"
- lodash "^4.17.13"
+ "@babel/types" "^7.10.5"
+ lodash "^4.17.19"
"@babel/helper-optimise-call-expression@^7.10.4":
version "7.10.4"
@@ -227,11 +226,11 @@
integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
"@babel/helper-regex@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.4.tgz#59b373daaf3458e5747dece71bbaf45f9676af6d"
- integrity sha512-inWpnHGgtg5NOF0eyHlC0/74/VkdRITY9dtTpB2PrxKKn+AkVMRiZz/Adrx+Ssg+MLDesi2zohBW6MVq6b4pOQ==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
+ integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==
dependencies:
- lodash "^4.17.13"
+ lodash "^4.17.19"
"@babel/helper-remap-async-to-generator@^7.10.4":
version "7.10.4"
@@ -302,15 +301,15 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.10.4", "@babel/parser@^7.6.0":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.4.tgz#9eedf27e1998d87739fb5028a5120557c06a1a64"
- integrity sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==
+"@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b"
+ integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ==
"@babel/plugin-proposal-async-generator-functions@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz#4b65abb3d9bacc6c657aaa413e56696f9f170fc6"
- integrity sha512-MJbxGSmejEFVOANAezdO39SObkURO5o/8b6fSH6D1pi9RZQt+ldppKPXfqgUWpSQ9asM6xaSaSJIaeWMDRP0Zg==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
+ integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/helper-remap-async-to-generator" "^7.10.4"
@@ -325,11 +324,11 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-proposal-decorators@^7.8.3":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.4.tgz#fe20ef10cc73f386f70910fca48798041cd357c7"
- integrity sha512-JHTWjQngOPv+ZQQqOGv2x6sCCr4IYWy7S1/VH6BE9ZfkoLrdQ2GpEP3tfb5M++G9PwvqjhY8VC/C3tXm+/eHvA==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4"
+ integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.10.4"
+ "@babel/helper-create-class-features-plugin" "^7.10.5"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-decorators" "^7.10.4"
@@ -514,12 +513,11 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-block-scoping@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.4.tgz#a670d1364bb5019a621b9ea2001482876d734787"
- integrity sha512-J3b5CluMg3hPUii2onJDRiaVbPtKFPLEaV5dOPY5OeAbDi1iU/UbbFFTgwb7WnanaDy7bjU35kc26W3eM5Qa0A==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.5.tgz#b81b8aafefbfe68f0f65f7ef397b9ece68a6037d"
+ integrity sha512-6Ycw3hjpQti0qssQcA6AMSFDHeNJ++R6dIMnpRqUjFeBBTmTDPa8zgF90OVfTvAo11mXZTlVUViY1g8ffrURLg==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
- lodash "^4.17.13"
"@babel/plugin-transform-classes@^7.10.4":
version "7.10.4"
@@ -602,11 +600,11 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-modules-amd@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.4.tgz#cb407c68b862e4c1d13a2fc738c7ec5ed75fc520"
- integrity sha512-3Fw+H3WLUrTlzi3zMiZWp3AR4xadAEMv6XRCYnd5jAlLM61Rn+CRJaZMaNvIpcJpQ3vs1kyifYvEVPFfoSkKOA==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1"
+ integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==
dependencies:
- "@babel/helper-module-transforms" "^7.10.4"
+ "@babel/helper-module-transforms" "^7.10.5"
"@babel/helper-plugin-utils" "^7.10.4"
babel-plugin-dynamic-import-node "^2.3.3"
@@ -621,12 +619,12 @@
babel-plugin-dynamic-import-node "^2.3.3"
"@babel/plugin-transform-modules-systemjs@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.4.tgz#8f576afd943ac2f789b35ded0a6312f929c633f9"
- integrity sha512-Tb28LlfxrTiOTGtZFsvkjpyjCl9IoaRI52AEU/VIwOwvDQWtbNJsAqTXzh+5R7i74e/OZHH2c2w2fsOqAfnQYQ==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85"
+ integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==
dependencies:
"@babel/helper-hoist-variables" "^7.10.4"
- "@babel/helper-module-transforms" "^7.10.4"
+ "@babel/helper-module-transforms" "^7.10.5"
"@babel/helper-plugin-utils" "^7.10.4"
babel-plugin-dynamic-import-node "^2.3.3"
@@ -661,9 +659,9 @@
"@babel/helper-replace-supers" "^7.10.4"
"@babel/plugin-transform-parameters@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.4.tgz#7b4d137c87ea7adc2a0f3ebf53266871daa6fced"
- integrity sha512-RurVtZ/D5nYfEg0iVERXYKEgDFeesHrHfx8RT05Sq57ucj2eOYAP6eu5fynL4Adju4I/mP/I6SO0DqNWAXjfLQ==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a"
+ integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
dependencies:
"@babel/helper-get-function-arity" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
@@ -690,9 +688,9 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-runtime@^7.9.6":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.4.tgz#594fb53453ea1b6f0779cceb48ce0718a447feb7"
- integrity sha512-8ULlGv8p+Vuxu+kz2Y1dk6MYS2b/Dki+NO6/0ZlfSj5tMalfDL7jI/o/2a+rrWLqSXvnadEqc2WguB4gdQIxZw==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.5.tgz#3b39b7b24830e0c2d8ff7a4489fe5cf99fbace86"
+ integrity sha512-tV4V/FjElJ9lQtyjr5xD2IFFbgY46r7EeVu5a8CpEKT5laheHKSlFeHjpkPppW3PqzGLAuv5k2qZX5LgVZIX5w==
dependencies:
"@babel/helper-module-imports" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
@@ -722,9 +720,9 @@
"@babel/helper-regex" "^7.10.4"
"@babel/plugin-transform-template-literals@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.4.tgz#e6375407b30fcb7fcfdbba3bb98ef3e9d36df7bc"
- integrity sha512-4NErciJkAYe+xI5cqfS8pV/0ntlY5N5Ske/4ImxAVX7mk9Rxt2bwDTGv1Msc2BRJvWQcmYEC+yoMLdX22aE4VQ==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c"
+ integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
@@ -833,21 +831,13 @@
esutils "^2.0.2"
"@babel/runtime-corejs2@^7.0.0":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.10.4.tgz#5d48ee239624d511c88208da86c27a161ee01cf7"
- integrity sha512-9sArmpZDQsnR1yyAcU51DxQrntWxt0LUKjPp3pIyo7kVLfaqKt8muppcT87QmFkXV5H50qXAF8JWOjk0jaXRYA==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.10.5.tgz#8daa1ceccc0468e5c2e15f124e3f51c2b3033b49"
+ integrity sha512-LJwyb1ac//Jr2zrGTTaNJhrP1wYCgVw9rzHbQPogKXCTLQ60EEWgeNtuqs6cLsq64O557SYzziCrOxNp0rRi8w==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
-"@babel/runtime-corejs3@^7.8.3":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d"
- integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==
- dependencies:
- core-js-pure "^3.0.0"
- regenerator-runtime "^0.13.4"
-
"@babel/runtime@7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f"
@@ -856,9 +846,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.6":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
- integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c"
+ integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==
dependencies:
regenerator-runtime "^0.13.4"
@@ -871,20 +861,20 @@
"@babel/parser" "^7.10.4"
"@babel/types" "^7.10.4"
-"@babel/traverse@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.4.tgz#e642e5395a3b09cc95c8e74a27432b484b697818"
- integrity sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564"
+ integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ==
dependencies:
"@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.10.4"
+ "@babel/generator" "^7.10.5"
"@babel/helper-function-name" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.10.4"
- "@babel/parser" "^7.10.4"
- "@babel/types" "^7.10.4"
+ "@babel/parser" "^7.10.5"
+ "@babel/types" "^7.10.5"
debug "^4.1.0"
globals "^11.1.0"
- lodash "^4.17.13"
+ lodash "^4.17.19"
"@babel/types@7.0.0-beta.38":
version "7.0.0-beta.38"
@@ -895,13 +885,13 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@babel/types@^7.10.4", "@babel/types@^7.4.4", "@babel/types@^7.6.0":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee"
- integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==
+"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.1", "@babel/types@^7.9.6":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15"
+ integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
- lodash "^4.17.13"
+ lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@cypress/listr-verbose-renderer@0.4.1":
@@ -1008,9 +998,9 @@
yargs "^8.0.2"
"@mdi/font@^5.0.45":
- version "5.3.45"
- resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.3.45.tgz#086d3ef77dee260c04dd5a593af602c250e5b315"
- integrity sha512-SD5d2vHEKRvDCInZQFXOwiFpBlzpuZOiqwxKf6E+zCt7UDc52TUSrL0+TXqY57VQh/SnTpZVXM+Uvs21OdPFWg==
+ version "5.4.55"
+ resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.4.55.tgz#f34263882251ac23f37c1312988e1f10256dc74c"
+ integrity sha512-M+Wdcs4nZ4/Kid949fcI0DsnvHtpE6pwk6Hv8YJZDp+Zne7ZtYdIN0z73cvcANkbyNnY3ncScULGMIceNd0xxQ==
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
@@ -1046,7 +1036,7 @@
"@nodelib/fs.scandir" "2.1.3"
fastq "^1.6.0"
-"@popperjs/core@^2.3.2":
+"@popperjs/core@^2.4.4":
version "2.4.4"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
@@ -1082,22 +1072,10 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
-"@types/babel-types@*", "@types/babel-types@^7.0.0":
- version "7.0.7"
- resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
- integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==
-
-"@types/babylon@^6.16.2":
- version "6.16.5"
- resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
- integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
- dependencies:
- "@types/babel-types" "*"
-
"@types/chai@^4.2.11":
- version "4.2.11"
- resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50"
- integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==
+ version "4.2.12"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.12.tgz#6160ae454cd89dae05adc3bb97997f488b608201"
+ integrity sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==
"@types/color-name@^1.1.1":
version "1.1.1"
@@ -1115,9 +1093,9 @@
integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
"@types/glob@^7.1.1":
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987"
- integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
+ integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
@@ -1145,16 +1123,16 @@
"@types/leaflet" "*"
"@types/leaflet@*", "@types/leaflet@^1.5.2":
- version "1.5.13"
- resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.13.tgz#e0e41612b236a6a37877a69a6eb996d2a9ba6905"
- integrity sha512-aCNOIeoukY8DIQUs/8bNiiKQKc75HSFwo1YqcFaLe+SkG4DL+0ygKCDfhfZ54UX8k28pn5uA3QJEAw3wa2hqHw==
+ version "1.5.17"
+ resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b"
+ integrity sha512-2XYq9k6kNjhNI7PaTz8Rdxcc8Vzwu97OaS9CtcrTxnTSxFUGwjlGjTDvhTLJU+JRSfZ4lBwGcl0SjZHALdVr6g==
dependencies:
"@types/geojson" "*"
"@types/lodash@^4.14.141":
- version "4.14.157"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8"
- integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==
+ version "4.14.158"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.158.tgz#b38ea8b6fe799acd076d7a8d7ab71c26ef77f785"
+ integrity sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w==
"@types/minimatch@*":
version "3.0.3"
@@ -1172,9 +1150,9 @@
integrity sha512-6nlq2eEh75JegDGUXis9wGTYIJpUvbori4qx++PRKQsV3YRkaqUNPNykzphniqPSZADXCouBuAnyptjUkMkhvw==
"@types/node@*", "@types/node@>=6":
- version "14.0.14"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce"
- integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==
+ version "14.0.27"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1"
+ integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -1247,9 +1225,9 @@
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
"@types/uglify-js@*":
- version "3.9.2"
- resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.2.tgz#01992579debba674e1e359cd6bcb1a1d0ab2e02b"
- integrity sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ==
+ version "3.9.3"
+ resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b"
+ integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w==
dependencies:
source-map "^0.6.1"
@@ -1271,18 +1249,18 @@
integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ==
"@types/webpack-sources@*":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.0.tgz#e58f1f05f87d39a5c64cf85705bdbdbb94d4d57e"
- integrity sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ==
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c"
+ integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw==
dependencies:
"@types/node" "*"
"@types/source-list-map" "*"
source-map "^0.7.3"
"@types/webpack@^4.4.31":
- version "4.41.18"
- resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.18.tgz#2945202617866ecdffa582087f1b6de04a7eed55"
- integrity sha512-mQm2R8vV2BZE/qIDVYqmBVLfX73a8muwjs74SpjEyJWJxeXBbsI9L65Pcia9XfYLYWzD1c1V8m+L0p30y2N7MA==
+ version "4.41.21"
+ resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee"
+ integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA==
dependencies:
"@types/anymatch" "*"
"@types/node" "*"
@@ -1316,14 +1294,14 @@
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
-"@typescript-eslint/experimental-utils@3.5.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.5.0.tgz#d09f9ffb890d1b15a7ffa9975fae92eee05597c4"
- integrity sha512-zGNOrVi5Wz0jcjUnFZ6QUD0MCox5hBuVwemGCew2qJzUX5xPoyR+0EzS5qD5qQXL/vnQ8Eu+nv03tpeFRwLrDg==
+"@typescript-eslint/experimental-utils@3.7.1":
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.1.tgz#ab036caaed4c870d22531d41f9352f3147364d61"
+ integrity sha512-TqE97pv7HrqWcGJbLbZt1v59tcqsSVpWTOf1AqrWK7n8nok2sGgVtYRuGXeNeLw3wXlLEbY1MKP3saB2HsO/Ng==
dependencies:
"@types/json-schema" "^7.0.3"
- "@typescript-eslint/types" "3.5.0"
- "@typescript-eslint/typescript-estree" "3.5.0"
+ "@typescript-eslint/types" "3.7.1"
+ "@typescript-eslint/typescript-estree" "3.7.1"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
@@ -1338,20 +1316,20 @@
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/parser@^3.0.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.5.0.tgz#9ff8c11877c48df24e10e19d7bf542ee0359500d"
- integrity sha512-sU07VbYB70WZHtgOjH/qfAp1+OwaWgrvD1Km1VXqRpcVxt971PMTU7gJtlrCje0M+Sdz7xKAbtiyIu+Y6QdnVA==
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.7.1.tgz#5d9ccecb116d12d9c6073e9861c57c9b1aa88128"
+ integrity sha512-W4QV/gXvfIsccN8225784LNOorcm7ch68Fi3V4Wg7gmkWSQRKevO4RrRqWo6N/Z/myK1QAiGgeaXN57m+R/8iQ==
dependencies:
"@types/eslint-visitor-keys" "^1.0.0"
- "@typescript-eslint/experimental-utils" "3.5.0"
- "@typescript-eslint/types" "3.5.0"
- "@typescript-eslint/typescript-estree" "3.5.0"
+ "@typescript-eslint/experimental-utils" "3.7.1"
+ "@typescript-eslint/types" "3.7.1"
+ "@typescript-eslint/typescript-estree" "3.7.1"
eslint-visitor-keys "^1.1.0"
-"@typescript-eslint/types@3.5.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.5.0.tgz#4e3d2a2272268d8ec3e3e4a37152a64956682639"
- integrity sha512-Dreqb5idi66VVs1QkbAwVeDmdJG+sDtofJtKwKCZXIaBsINuCN7Jv5eDIHrS0hFMMiOvPH9UuOs4splW0iZe4Q==
+"@typescript-eslint/types@3.7.1":
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.7.1.tgz#90375606b2fd73c1224fe9e397ee151e28fa1e0c"
+ integrity sha512-PZe8twm5Z4b61jt7GAQDor6KiMhgPgf4XmUb9zdrwTbgtC/Sj29gXP1dws9yEn4+aJeyXrjsD9XN7AWFhmnUfg==
"@typescript-eslint/typescript-estree@2.34.0":
version "2.34.0"
@@ -1366,13 +1344,13 @@
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/typescript-estree@3.5.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.5.0.tgz#dfc895db21a381b84f24c2a719f5bf9c600dcfdc"
- integrity sha512-Na71ezI6QP5WVR4EHxwcBJgYiD+Sre9BZO5iJK2QhrmRPo/42+b0no/HZIrdD1sjghzlYv7t+7Jis05M1uMxQg==
+"@typescript-eslint/typescript-estree@3.7.1":
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.1.tgz#ce1ffbd0fa53f34d4ce851a7a364e392432f6eb3"
+ integrity sha512-m97vNZkI08dunYOr2lVZOHoyfpqRs0KDpd6qkGaIcLGhQ2WPtgHOd/eVbsJZ0VYCQvupKrObAGTOvk3tfpybYA==
dependencies:
- "@typescript-eslint/types" "3.5.0"
- "@typescript-eslint/visitor-keys" "3.5.0"
+ "@typescript-eslint/types" "3.7.1"
+ "@typescript-eslint/visitor-keys" "3.7.1"
debug "^4.1.1"
glob "^7.1.6"
is-glob "^4.0.1"
@@ -1380,10 +1358,10 @@
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/visitor-keys@3.5.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.5.0.tgz#73c1ea2582f814735e4afdc1cf6f5e3af78db60a"
- integrity sha512-7cTp9rcX2sz9Z+zua9MCOX4cqp5rYyFD5o8LlbSpXrMTXoRdngTtotRZEkm8+FNMHPWYFhitFK+qt/brK8BVJQ==
+"@typescript-eslint/visitor-keys@3.7.1":
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.1.tgz#b90191e74efdee656be8c5a30f428ed16dda46d1"
+ integrity sha512-xn22sQbEya+Utj2IqJHGLA3i1jDzR43RzWupxojbSWnj3nnPLavaQmWe5utw03CwYao3r00qzXfgJMGNkrzrAA==
dependencies:
eslint-visitor-keys "^1.1.0"
@@ -1636,9 +1614,9 @@
strip-ansi "^6.0.0"
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz#8213a5ff3202f9f2137fe55370f9e8b9656081c3"
- integrity sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d"
+ integrity sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw==
dependencies:
consolidate "^0.15.1"
hash-sum "^1.0.2"
@@ -1676,9 +1654,9 @@
vue-eslint-parser "^7.0.0"
"@vue/preload-webpack-plugin@^1.1.0":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.1.tgz#18723530d304f443021da2292d6ec9502826104a"
- integrity sha512-8VCoJeeH8tCkzhkpfOkt+abALQkS11OIHhte5MBzYaKMTqK0A3ZAKEUVAffsOklhEv7t0yrQt696Opnu9oAx+w==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz#ceb924b4ecb3b9c43871c7a429a02f8423e621ab"
+ integrity sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==
"@vue/test-utils@1.0.3":
version "1.0.3"
@@ -1894,13 +1872,6 @@ acorn-dynamic-import@^4.0.0:
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
-acorn-globals@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
- integrity sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=
- dependencies:
- acorn "^4.0.4"
-
acorn-globals@^4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
@@ -1924,16 +1895,6 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
-acorn@^3.1.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
- integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
-
-acorn@^4.0.4, acorn@~4.0.2:
- version "4.0.13"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
- integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
-
acorn@^6.0.1, acorn@^6.1.1, acorn@^6.4.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
@@ -1975,9 +1936,9 @@ ajv-errors@^1.0.0:
integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773"
- integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+ integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@5, ajv@^5.5.1:
version "5.5.2"
@@ -1989,25 +1950,16 @@ ajv@5, ajv@^5.5.1:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
-ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5:
- version "6.12.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
- integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3:
+ version "6.12.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
+ integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-align-text@^0.1.1, align-text@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
- integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=
- dependencies:
- kind-of "^3.0.2"
- longest "^1.0.1"
- repeat-string "^1.5.2"
-
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -2463,6 +2415,11 @@ asn1@~0.2.3:
dependencies:
safer-buffer "~2.1.0"
+assert-never@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
+ integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
+
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
@@ -2486,12 +2443,7 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
-ast-types@0.12.4, ast-types@^0.12.2:
- version "0.12.4"
- resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.4.tgz#71ce6383800f24efc9a1a3308f3a6e420a0974d1"
- integrity sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw==
-
-ast-types@0.13.3, ast-types@^0.13.2, ast-types@~0.13.2:
+ast-types@0.13.3, ast-types@^0.13.2, ast-types@^0.13.3, ast-types@~0.13.2:
version "0.13.3"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7"
integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==
@@ -2546,12 +2498,12 @@ atob@^2.1.2:
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autoprefixer@^9.8.0:
- version "9.8.4"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.4.tgz#736f1012673a70fa3464671d78d41abd54512863"
- integrity sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==
+ version "9.8.5"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa"
+ integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg==
dependencies:
browserslist "^4.12.0"
- caniuse-lite "^1.0.30001087"
+ caniuse-lite "^1.0.30001097"
colorette "^1.2.0"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
@@ -2632,15 +2584,12 @@ babel-runtime@6.26.0, babel-runtime@^6.25.0, babel-runtime@^6.26.0:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
-babel-types@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
- integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
+babel-walk@3.0.0-canary-5:
+ version "3.0.0-canary-5"
+ resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
+ integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
dependencies:
- babel-runtime "^6.26.0"
- esutils "^2.0.2"
- lodash "^4.17.4"
- to-fast-properties "^1.0.3"
+ "@babel/types" "^7.9.6"
babylon@^6.18.0:
version "6.18.0"
@@ -2942,12 +2891,12 @@ browserslist@4.7.0:
node-releases "^1.1.29"
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5:
- version "4.12.2"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.2.tgz#76653d7e4c57caa8a1a28513e2f4e197dc11a711"
- integrity sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==
+ version "4.13.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d"
+ integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==
dependencies:
- caniuse-lite "^1.0.30001088"
- electron-to-chromium "^1.3.483"
+ caniuse-lite "^1.0.30001093"
+ electron-to-chromium "^1.3.488"
escalade "^3.0.1"
node-releases "^1.1.58"
@@ -3178,11 +3127,6 @@ camelcase-keys@^2.0.0:
camelcase "^2.0.0"
map-obj "^1.0.0"
-camelcase@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
- integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=
-
camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -3208,10 +3152,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001087, caniuse-lite@^1.0.30001088:
- version "1.0.30001093"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001093.tgz#833e80f64b1a0455cbceed2a4a3baf19e4abd312"
- integrity sha512-0+ODNoOjtWD5eS9aaIpf4K0gQqZfILNY4WSNuYzeT1sXni+lMrrVjc0odEobJt6wrODofDZUX8XYi/5y7+xl8g==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097:
+ version "1.0.30001109"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001109.tgz#a9f3f26a0c3753b063d7acbb48dfb9c0e46f2b19"
+ integrity sha512-4JIXRodHzdS3HdK8nSgIqXYLExOvG+D2/EenSvcub2Kp3QEADjo2v2oUn5g0n0D+UNwG9BtwKOyGcSq2qvQXvQ==
capture-stack-trace@^1.0.0:
version "1.0.1"
@@ -3233,14 +3177,6 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17"
integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==
-center-align@^0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
- integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60=
- dependencies:
- align-text "^0.1.3"
- lazy-cache "^1.0.3"
-
chai@^4.1.2:
version "4.2.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
@@ -3337,7 +3273,7 @@ character-entities@^1.0.0:
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
-character-parser@^2.1.1:
+character-parser@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
@@ -3388,10 +3324,10 @@ chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.8:
optionalDependencies:
fsevents "^1.2.7"
-chokidar@^3.3.0, chokidar@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8"
- integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==
+chokidar@^3.3.0, chokidar@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1"
+ integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==
dependencies:
anymatch "~3.1.1"
braces "~3.0.2"
@@ -3453,7 +3389,7 @@ classnames@^2.2.6:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
-clean-css@4.2.x, clean-css@^4.1.11:
+clean-css@4.2.x:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
@@ -3530,9 +3466,9 @@ cli-spinners@^0.1.2:
integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=
cli-spinners@^2.0.0, cli-spinners@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5"
- integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w==
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f"
+ integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==
cli-truncate@^0.2.1:
version "0.2.1"
@@ -3575,15 +3511,6 @@ clipboardy@^2.3.0:
execa "^1.0.0"
is-wsl "^2.1.1"
-cliui@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
- integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=
- dependencies:
- center-align "^0.1.1"
- right-align "^0.1.1"
- wordwrap "0.0.2"
-
cliui@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
@@ -3671,9 +3598,9 @@ code-point-at@^1.0.0:
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror@^5.39.0:
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.55.0.tgz#23731f641288f202a6858fdc878f3149e0e04363"
- integrity sha512-TumikSANlwiGkdF/Blnu/rqovZ0Y3Jh8yy9TqrPbSM0xxSucq3RgnpVDQ+mD9q6JERJEIT2FMuF/fBGfkhIR/g==
+ version "5.56.0"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.56.0.tgz#675640fcc780105cd22d3faa738b5d7ea6426f61"
+ integrity sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw==
collapse-white-space@^1.0.2:
version "1.0.6"
@@ -3729,9 +3656,9 @@ color@^3.0.0:
color-string "^1.5.2"
colorette@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.0.tgz#45306add826d196e8c87236ac05d797f25982e63"
- integrity sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
+ integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colors@^1.1.2:
version "1.4.0"
@@ -3948,15 +3875,13 @@ constant-case@^2.0.0:
snake-case "^2.1.0"
upper-case "^1.1.1"
-constantinople@^3.0.1, constantinople@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.2.tgz#d45ed724f57d3d10500017a7d3a889c1381ae647"
- integrity sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==
+constantinople@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
+ integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
dependencies:
- "@types/babel-types" "^7.0.0"
- "@types/babylon" "^6.16.2"
- babel-types "^6.26.0"
- babylon "^6.18.0"
+ "@babel/parser" "^7.6.0"
+ "@babel/types" "^7.6.1"
constants-browserify@^1.0.0:
version "1.0.0"
@@ -4040,11 +3965,6 @@ core-js-compat@^3.6.2, core-js-compat@^3.6.5:
browserslist "^4.8.5"
semver "7.0.0"
-core-js-pure@^3.0.0:
- version "3.6.5"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
- integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-
core-js@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4"
@@ -4398,9 +4318,9 @@ cssstyle@^2.0.0:
cssom "~0.3.6"
csstype@^2.6.5:
- version "2.6.11"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.11.tgz#452f4d024149ecf260a852b025e36562a253ffc5"
- integrity sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==
+ version "2.6.13"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
+ integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
cucumber-html-reporter@^3.0.4:
version "3.0.4"
@@ -4519,18 +4439,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
-decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-decamelize@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-3.2.0.tgz#84b8e8f4f8c579f938e35e2cc7024907e0090851"
- integrity sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==
- dependencies:
- xregexp "^4.2.4"
-
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -4985,10 +4898,10 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
-electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.483:
- version "1.3.487"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.487.tgz#8075e6ea33ee2e79a2dfb2a2467033f014017258"
- integrity sha512-m4QS3IDShxauFfYFpnEzRCcUI55oKB9acEnHCuY/hSCZMz9Pz2KJj+UBnGHxRxS/mS1aphqOQ5wI6gc3yDZ7ew==
+electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.488:
+ version "1.3.514"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.514.tgz#107499c28cb3c09fe6a863c19fc2202d5d9e8e41"
+ integrity sha512-8vb8zKIeGlZigeDzNWWthmGeLzo5CC43Lc+CZshMs7UXFVMPNLtXJGa/txedpu3OJFrXXVheBwp9PqOJJlHQ8w==
elegant-spinner@^1.0.1:
version "1.0.1"
@@ -5039,11 +4952,11 @@ encodeurl@~1.0.2:
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
encoding@^0.1.11:
- version "0.1.12"
- resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
- integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
dependencies:
- iconv-lite "~0.4.13"
+ iconv-lite "^0.6.2"
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
@@ -5061,10 +4974,10 @@ enhanced-resolve@^0.9.1:
memory-fs "^0.2.0"
tapable "^0.1.8"
-enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d"
- integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==
+enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126"
+ integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==
dependencies:
graceful-fs "^4.1.2"
memory-fs "^0.5.0"
@@ -5145,9 +5058,9 @@ es6-promisify@^5.0.0:
es6-promise "^4.0.3"
escalade@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.1.tgz#52568a77443f6927cd0ab9c73129137533c965ed"
- integrity sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
+ integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
escape-html@~1.0.3:
version "1.0.3"
@@ -5429,9 +5342,9 @@ eventemitter3@^4.0.0:
integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
events@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
- integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
+ integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
eventsource@^1.0.7:
version "1.0.7"
@@ -6650,9 +6563,9 @@ graphql-static-binding@0.9.3:
cucumber-html-reporter "^3.0.4"
graphql-tag@^2.10.3:
- version "2.10.3"
- resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03"
- integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
+ integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==
graphql@0.11.3:
version "0.11.3"
@@ -6676,16 +6589,16 @@ graphql@^0.13.1:
iterall "^1.2.1"
graphql@^14.0.0, graphql@^14.0.2:
- version "14.6.0"
- resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49"
- integrity sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg==
+ version "14.7.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"
+ integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==
dependencies:
iterall "^1.2.2"
graphql@^15.0.0:
- version "15.2.0"
- resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.2.0.tgz#d9c655a523a3196d4b23657ec6ec5963b3bd4970"
- integrity sha512-tsceRyHfgzZo+ee0YK3o8f0CR0cXAXxRlxoORWFo/CoM1bVy3UXGWeyzBcf+Y6oqPvO27BDmOEVATcunOO/MrQ==
+ version "15.3.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"
+ integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==
growl@1.10.5:
version "1.10.5"
@@ -6719,11 +6632,11 @@ har-schema@^2.0.0:
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
har-validator@~5.1.0, har-validator@~5.1.3:
- version "5.1.3"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
- integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ version "5.1.5"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
+ integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
dependencies:
- ajv "^6.5.5"
+ ajv "^6.12.3"
har-schema "^2.0.0"
has-ansi@^2.0.0:
@@ -6847,14 +6760,14 @@ hex-color-regex@^1.1.0:
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
highlight.js@^9.6.0:
- version "9.18.1"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
- integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==
+ version "9.18.3"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634"
+ integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
-highlight.js@~9.16.0:
- version "9.16.2"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.16.2.tgz#68368d039ffe1c6211bcc07e483daf95de3e403e"
- integrity sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw==
+highlight.js@~10.1.0:
+ version "10.1.2"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c"
+ integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA==
hmac-drbg@^1.0.0:
version "1.0.1"
@@ -7064,17 +6977,24 @@ human-signals@^1.1.1:
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
hyphenate-style-name@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
- integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
+ integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
+iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+ integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -7293,9 +7213,9 @@ inquirer@6.5.0:
through "^2.3.6"
inquirer@^7.0.0, inquirer@^7.1.0:
- version "7.3.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.0.tgz#aa3e7cb0c18a410c3c16cdd2bc9dcbe83c4d333e"
- integrity sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA==
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+ integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
dependencies:
ansi-escapes "^4.2.1"
chalk "^4.1.0"
@@ -7303,7 +7223,7 @@ inquirer@^7.0.0, inquirer@^7.1.0:
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
- lodash "^4.17.15"
+ lodash "^4.17.19"
mute-stream "0.0.8"
run-async "^2.4.0"
rxjs "^6.6.0"
@@ -7530,13 +7450,13 @@ is-docker@^2.0.0:
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
-is-expression@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f"
- integrity sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=
+is-expression@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
+ integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
dependencies:
- acorn "~4.0.2"
- object-assign "^4.0.1"
+ acorn "^7.1.1"
+ object-assign "^4.1.1"
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
@@ -7889,9 +7809,9 @@ javascript-stringify@^2.0.0, javascript-stringify@^2.0.1:
integrity sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow==
javascript-time-ago@^2.0.4:
- version "2.0.8"
- resolved "https://registry.yarnpkg.com/javascript-time-ago/-/javascript-time-ago-2.0.8.tgz#708e5d507b9e1a4ea0c25987b0d9a1a86e8ee229"
- integrity sha512-/cQbnAwmF2OgpMCg8r185ZGqkkoHE8paNn9T3S98A6DXFQn4irzMVgMSWnKB4jvcvzTauF3HRi9wZXUdwHyj6Q==
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/javascript-time-ago/-/javascript-time-ago-2.0.13.tgz#aa11f80887ce044f851a78c8761861e436ed0954"
+ integrity sha512-zH+obXUQ4vlc9UlERFe637rNJQaVYLizwODUfGzYN/cNW/owkk5wzb327gAfEXFpI4yhFcStEaoqoJtMGAmrAg==
dependencies:
relative-time-format "^0.1.3"
@@ -7904,9 +7824,9 @@ jest-worker@^25.4.0:
supports-color "^7.0.0"
js-base64@^2.1.8, js-base64@^2.1.9, js-base64@^2.3.2:
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.2.tgz#cf9301bc5cc756892a9a6c8d7138322e5944fb0d"
- integrity sha512-1hgLrLIrmCgZG+ID3VoLNLOSwjGnoZa8tyrUdEteMeIzsT6PH7PMLyUvbDwzNE56P3PNxyvuIOx4Uh2E5rzQIw==
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.3.tgz#7afdb9b57aa7717e15d370b66e8f36a9cb835dc3"
+ integrity sha512-fiUvdfCaAXoQTHdKMgTvg6IkecXDcVz6V5rlftUTclF9IKBjMizvSdQaCl/z/6TApDeby5NL+axYou3i0mu1Pg==
js-beautify@^1.6.12:
version "1.11.0"
@@ -7931,7 +7851,7 @@ js-queue@2.0.0:
dependencies:
easy-stack "^1.0.0"
-js-stringify@^1.0.1:
+js-stringify@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
@@ -8304,11 +8224,6 @@ lazy-ass@1.6.0:
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
-lazy-cache@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
- integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4=
-
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -8618,7 +8533,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@4.17.15, lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10:
+lodash@4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -8628,6 +8543,11 @@ lodash@4.17.5:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
+lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10:
+ version "4.17.19"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+ integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
+
log-symbols@2.2.0, log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -8675,11 +8595,6 @@ longest-streak@^2.0.1:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
-longest@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
- integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
-
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -8717,13 +8632,13 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-lowlight@1.13.1:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb"
- integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ==
+lowlight@^1.14.0:
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108"
+ integrity sha512-N2E7zTM7r1CwbzwspPxJvmjAbxljCPThTFawEX2Z7+P3NGrrvY54u8kyU16IY4qWfoVIxY8SYCS8jTkuG7TqYA==
dependencies:
fault "^1.0.0"
- highlight.js "~9.16.0"
+ highlight.js "~10.1.0"
lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3, lru-cache@^4.1.5:
version "4.1.5"
@@ -9094,9 +9009,9 @@ minipass-flush@^1.0.5:
minipass "^3.0.0"
minipass-pipeline@^1.2.2:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34"
- integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
+ integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
dependencies:
minipass "^3.0.0"
@@ -9158,7 +9073,7 @@ mkdirp@0.5.4:
dependencies:
minimist "^1.2.5"
-"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -9240,9 +9155,9 @@ move-concurrently@^1.0.1:
run-queue "^1.0.3"
mri@^1.1.4:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.5.tgz#ce21dba2c69f74a9b7cf8a1ec62307e089e223e0"
- integrity sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg==
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"
+ integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==
ms@2.0.0:
version "2.0.0"
@@ -9324,9 +9239,9 @@ negotiator@0.6.2:
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
- integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ngeohash@^0.6.3:
version "0.6.3"
@@ -9440,9 +9355,9 @@ node-ipc@^9.1.1:
vm-browserify "^1.0.1"
node-releases@^1.1.29, node-releases@^1.1.3, node-releases@^1.1.58:
- version "1.1.58"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935"
- integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==
+ version "1.1.60"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
+ integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
node-request-by-swagger@^1.0.6:
version "1.1.4"
@@ -9842,9 +9757,9 @@ ora@^3.0.0, ora@^3.4.0:
wcwidth "^1.0.1"
ora@^4.0.2:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d"
- integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.5.tgz#7410b5cc2d99fa637fd5099bbb9f02bfbb5a361e"
+ integrity sha512-jCDgm9DqvRcNIAEv2wZPrh7E5PcQiDUnbnWbAfu4NGAE2ZNqPFbDixmWldy1YG2QfLeQhuiu6/h5VRrk6cG50w==
dependencies:
chalk "^3.0.0"
cli-cursor "^3.1.0"
@@ -10100,9 +10015,9 @@ parse-json@^4.0.0:
json-parse-better-errors "^1.0.1"
parse-json@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
- integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878"
+ integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==
dependencies:
"@babel/code-frame" "^7.0.0"
error-ex "^1.3.1"
@@ -10275,9 +10190,9 @@ performance-now@^2.1.0:
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
phoenix@^1.4.11:
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.5.3.tgz#994487b67adc8ca683f76c41dca3271e476184bd"
- integrity sha512-wcyHTac54uxpyycKsdXfSpN+5YuYwaXQnkLahxseznDn+V5K3mIEJg+d6QhLBdrn5/0pWIPE7bBC6SBswnIvOg==
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.5.4.tgz#d42bb537f03f55076b4e7a6757fe29318a8439f0"
+ integrity sha512-mTxseCKWDgrBQRIriqzvxL+QH5xruu6KQPqFdDx0jrdu/nqWCo914MLihVksn7SV2Bol3T+e/VtovJgC5UZT+w==
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
@@ -10364,13 +10279,13 @@ popper.js@^1.15.0:
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
portfinder@^1.0.26:
- version "1.0.26"
- resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70"
- integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==
+ version "1.0.28"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
+ integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
dependencies:
async "^2.6.2"
debug "^3.1.1"
- mkdirp "^0.5.1"
+ mkdirp "^0.5.5"
posix-character-classes@^0.1.0:
version "0.1.1"
@@ -10530,14 +10445,14 @@ postcss-modules-local-by-default@^2.0.6:
postcss-value-parser "^3.3.1"
postcss-modules-local-by-default@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
- integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
+ integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
dependencies:
icss-utils "^4.1.1"
- postcss "^7.0.16"
+ postcss "^7.0.32"
postcss-selector-parser "^6.0.2"
- postcss-value-parser "^4.0.0"
+ postcss-value-parser "^4.1.0"
postcss-modules-scope@^2.1.0, postcss-modules-scope@^2.2.0:
version "2.2.0"
@@ -10722,7 +10637,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
-postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
@@ -10737,7 +10652,7 @@ postcss@^5.2.17:
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
version "7.0.32"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
@@ -10762,9 +10677,9 @@ posthtml-rename-id@^1.0:
escape-string-regexp "1.0.5"
posthtml-render@^1.0.5, posthtml-render@^1.0.6:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.2.tgz#f554a19ed40d40e2bfc160826b0a91d4a23656cd"
- integrity sha512-MbIXTWwAfJ9qET6Zl29UNwJcDJEEz9Zkr5oDhiujitJa7YBJwEpbkX2cmuklCDxubTMoRWpid3q8DrSyGnUUzQ==
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.3.tgz#da1cf7ba4efb42cfe9c077f4f41669745de99b6d"
+ integrity sha512-rGGayND//VwTlsYKNqdILsA7U/XP0WJa6SMcdAEoqc2WRM5QExplGg/h9qbTuHz7mc2PvaXU+6iNxItvr5aHMg==
posthtml-svg-mode@^1.0.3:
version "1.0.3"
@@ -10941,14 +10856,14 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
-prosemirror-collab@1.2.2:
+prosemirror-collab@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.2.2.tgz#8d2c0e82779cfef5d051154bd0836428bd6d9c4a"
integrity sha512-tBnHKMLgy5Qmx9MYVcLfs3pAyjtcqYYDd9kp3y+LSiQzkhMQDfZSV3NXWe4Gsly32adSef173BvObwfoSQL5MA==
dependencies:
prosemirror-state "^1.0.0"
-prosemirror-commands@1.1.4:
+prosemirror-commands@1.1.4, prosemirror-commands@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.1.4.tgz#991563e67623acab4f8c510fad1570f8b4693780"
integrity sha512-kj4Qi+8h3EpJtZuuEDwZ9h2/QNGWDsIX/CzjmClxi9GhxWyBUMVUvIFk0mgdqHyX20lLeGmOpc0TLA5aPzgpWg==
@@ -10976,7 +10891,7 @@ prosemirror-gapcursor@1.1.5:
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
-prosemirror-history@1.1.3:
+prosemirror-history@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.1.3.tgz#4f76a1e71db4ef7cdf0e13dec6d8da2aeaecd489"
integrity sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==
@@ -10985,7 +10900,7 @@ prosemirror-history@1.1.3:
prosemirror-transform "^1.0.0"
rope-sequence "^1.3.0"
-prosemirror-inputrules@1.1.2:
+prosemirror-inputrules@1.1.2, prosemirror-inputrules@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz#487e46c763e1212a4577397aba7706139084f012"
integrity sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g==
@@ -10993,15 +10908,7 @@ prosemirror-inputrules@1.1.2:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
-prosemirror-keymap@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.3.tgz#be22d6108df2521608e9216a87b1a810f0ed361e"
- integrity sha512-PRA4NzkUMzV/NFf5pyQ6tmlIHiW/qjQ1kGWUlV2rF/dvlOxtpGpTEjIMhWgLuMf+HiDEFnUEP7uhYXu+t+491g==
- dependencies:
- prosemirror-state "^1.0.0"
- w3c-keyname "^2.2.0"
-
-prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
+prosemirror-keymap@1.1.4, prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz#8b481bf8389a5ac40d38dbd67ec3da2c7eac6a6d"
integrity sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg==
@@ -11009,14 +10916,14 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
-prosemirror-model@1.9.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.8.1:
+prosemirror-model@1.10.0, prosemirror-model@1.9.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.10.0, prosemirror-model@^1.8.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.9.1.tgz#8c08cf556f593c5f015548d2c1a6825661df087f"
integrity sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q==
dependencies:
orderedmap "^1.1.0"
-prosemirror-schema-list@1.1.2:
+prosemirror-schema-list@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz#310809209094b03425da7f5c337105074913da6c"
integrity sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==
@@ -11024,7 +10931,7 @@ prosemirror-schema-list@1.1.2:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
-prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1:
+prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.3.tgz#b2862866b14dec2b3ae1ab18229f2bd337651a2c"
integrity sha512-PLXh2VJsIgvlgSTH6I2Yg6vk1CzPDp21DFreVpQtDMY2S6WaMmrQgDTLRcsrD8X38v8Yc873H7+ogdGzyIPn+w==
@@ -11032,10 +10939,10 @@ prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, pro
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
-prosemirror-tables@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.0.0.tgz#ec3d0b11e638c6a92dd14ae816d0a2efd1719b70"
- integrity sha512-zFw5Us4G5Vdq0yIj8GiqZOGA6ud5UKpMKElux9O0HrfmhkuGa1jf1PCpz2R5pmIQJv+tIM24H1mox/ODBAX37Q==
+prosemirror-tables@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.1.0.tgz#e7fc65e57a44759b0b999d8c71294f79e5a4d54b"
+ integrity sha512-E00+KSbDw65966GdiLBpqTNxIextw0RavlGmvdv/dyYbN9OTD0gzaoCU1S8MAbz4GLKmY9Y/g4nSiC1IL1ThQg==
dependencies:
prosemirror-keymap "^1.1.2"
prosemirror-model "^1.8.1"
@@ -11043,38 +10950,31 @@ prosemirror-tables@1.0.0:
prosemirror-transform "^1.2.1"
prosemirror-view "^1.13.3"
-prosemirror-transform@1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.5.tgz#7a3e2c61fcdbaf1d0844a2a3bc34fc3524e9809c"
- integrity sha512-eqeIaxWtUfOnpA1ERrXCuSIMzqIJtL9Qrs5uJMCjY5RMSaH5o4pc390SAjn/IDPeIlw6auh0hCCXs3wRvGnQug==
+prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.2.6:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.7.tgz#ba0e291a3cb43e6b633b779d93f53d01f5dad570"
+ integrity sha512-/107Lo2zeDgXuJBxb8s/clNu0Z2W8Gv3MKmkuSS/68Mcr7LBaUnN/Hj2g+GUxEJ7MpExCzFs65GrsNo2K9rxUQ==
dependencies:
prosemirror-model "^1.0.0"
-prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.6.tgz#b3ad86e976c49f7dd541cc39e0d7215bcfe7b596"
- integrity sha512-DyV6cRip8//GIHTrqBe2B7I8VjPFQZYuBuB4clpguq1SrS9lLponoBt/0XRWxETkCVsxYSvwE76X0zo6AZhwaw==
- dependencies:
- prosemirror-model "^1.0.0"
-
-prosemirror-utils@0.9.6:
+prosemirror-utils@^0.9.6:
version "0.9.6"
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
-prosemirror-view@1.14.7:
- version "1.14.7"
- resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.14.7.tgz#5480f2ec7f091e616989894983b62c5e2d16edc1"
- integrity sha512-ZCRbGAmJa0ORIY4xrDvOpxS/oAnph3egDauvQEI7SX4eex0zovUfC61I5X4AtPCaNN4JpLWEk60voCWi0cE2vA==
+prosemirror-view@1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.0.tgz#372102c91d05b3b0f371b3eb59aeacedb5011bba"
+ integrity sha512-a7Q76sO/DCZr2UX2Rv1Rbw52cr9kVIz8iJOf/rq4mPN1NA3lugq2BKJgUMwlB3U4utyw3olLigqouRHM48NJyg==
dependencies:
prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
-prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.0.tgz#372102c91d05b3b0f371b3eb59aeacedb5011bba"
- integrity sha512-a7Q76sO/DCZr2UX2Rv1Rbw52cr9kVIz8iJOf/rq4mPN1NA3lugq2BKJgUMwlB3U4utyw3olLigqouRHM48NJyg==
+prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.15.0:
+ version "1.15.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.2.tgz#3f07881d11f18c033467591bbaec26b569bbc22c"
+ integrity sha512-0wftmMDVD8VXj2HZgv6Rg//+tgJC0lpV9LkYlCiAkDLKsf4yW3Ozs5td1ZXqsyoqvX0ga/k5g2EyLbqOMmC1+w==
dependencies:
prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0"
@@ -11125,110 +11025,108 @@ public-encrypt@^4.0.0:
randombytes "^2.0.1"
safe-buffer "^5.1.2"
-pug-attrs@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-2.0.4.tgz#b2f44c439e4eb4ad5d4ef25cac20d18ad28cc336"
- integrity sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==
+pug-attrs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
+ integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
dependencies:
- constantinople "^3.0.1"
- js-stringify "^1.0.1"
- pug-runtime "^2.0.5"
+ constantinople "^4.0.1"
+ js-stringify "^1.0.2"
+ pug-runtime "^3.0.0"
-pug-code-gen@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2"
- integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==
+pug-code-gen@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.1.tgz#ff3b337b100c494ea63ef766091d27f7d73acb7e"
+ integrity sha512-xJIGvmXTQlkJllq6hqxxjRWcay2F9CU69TuAuiVZgHK0afOhG5txrQOcZyaPHBvSWCU/QQOqEp5XCH94rRZpBQ==
dependencies:
- constantinople "^3.1.2"
+ constantinople "^4.0.1"
doctypes "^1.1.0"
- js-stringify "^1.0.1"
- pug-attrs "^2.0.4"
- pug-error "^1.3.3"
- pug-runtime "^2.0.5"
- void-elements "^2.0.1"
- with "^5.0.0"
+ js-stringify "^1.0.2"
+ pug-attrs "^3.0.0"
+ pug-error "^2.0.0"
+ pug-runtime "^3.0.0"
+ void-elements "^3.1.0"
+ with "^7.0.0"
-pug-error@^1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-1.3.3.tgz#f342fb008752d58034c185de03602dd9ffe15fa6"
- integrity sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==
+pug-error@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
+ integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
-pug-filters@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-3.1.1.tgz#ab2cc82db9eeccf578bda89130e252a0db026aa7"
- integrity sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==
+pug-filters@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
+ integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
dependencies:
- clean-css "^4.1.11"
- constantinople "^3.0.1"
+ constantinople "^4.0.1"
jstransformer "1.0.0"
- pug-error "^1.3.3"
- pug-walk "^1.1.8"
- resolve "^1.1.6"
- uglify-js "^2.6.1"
+ pug-error "^2.0.0"
+ pug-walk "^2.0.0"
+ resolve "^1.15.1"
-pug-lexer@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-4.1.0.tgz#531cde48c7c0b1fcbbc2b85485c8665e31489cfd"
- integrity sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==
+pug-lexer@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.0.tgz#0b779e7d8cbf0f103803675be96351942fd9a727"
+ integrity sha512-52xMk8nNpuyQ/M2wjZBN5gXQLIylaGkAoTk5Y1pBhVqaopaoj8Z0iVzpbFZAqitL4RHNVDZRnJDsqEYe99Ti0A==
dependencies:
- character-parser "^2.1.1"
- is-expression "^3.0.0"
- pug-error "^1.3.3"
+ character-parser "^2.2.0"
+ is-expression "^4.0.0"
+ pug-error "^2.0.0"
-pug-linker@^3.0.6:
- version "3.0.6"
- resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-3.0.6.tgz#f5bf218b0efd65ce6670f7afc51658d0f82989fb"
- integrity sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==
+pug-linker@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
+ integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
dependencies:
- pug-error "^1.3.3"
- pug-walk "^1.1.8"
+ pug-error "^2.0.0"
+ pug-walk "^2.0.0"
-pug-load@^2.0.12:
- version "2.0.12"
- resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-2.0.12.tgz#d38c85eb85f6e2f704dea14dcca94144d35d3e7b"
- integrity sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==
+pug-load@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
+ integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
dependencies:
- object-assign "^4.1.0"
- pug-walk "^1.1.8"
+ object-assign "^4.1.1"
+ pug-walk "^2.0.0"
-pug-parser@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-5.0.1.tgz#03e7ada48b6840bd3822f867d7d90f842d0ffdc9"
- integrity sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==
+pug-parser@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
+ integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
dependencies:
- pug-error "^1.3.3"
- token-stream "0.0.1"
+ pug-error "^2.0.0"
+ token-stream "1.0.0"
-pug-runtime@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-2.0.5.tgz#6da7976c36bf22f68e733c359240d8ae7a32953a"
- integrity sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==
+pug-runtime@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.0.tgz#d523025fdc0a1efe70929d1fd3a2d24121ffffb6"
+ integrity sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA==
-pug-strip-comments@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz#cc1b6de1f6e8f5931cf02ec66cdffd3f50eaf8a8"
- integrity sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==
+pug-strip-comments@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
+ integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
dependencies:
- pug-error "^1.3.3"
+ pug-error "^2.0.0"
-pug-walk@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-1.1.8.tgz#b408f67f27912f8c21da2f45b7230c4bd2a5ea7a"
- integrity sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==
+pug-walk@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
+ integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
-pug@^2.0.3:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/pug/-/pug-2.0.4.tgz#ee7682ec0a60494b38d48a88f05f3b0ac931377d"
- integrity sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==
+pug@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.0.tgz#101eecd7a236cd9906e420e17799d4d57f2b7d93"
+ integrity sha512-inmsJyFBSHZaiGLaguoFgJGViX0If6AcfcElimvwj9perqjDpUpw79UIEDZbWFmoGVidh08aoE+e8tVkjVJPCw==
dependencies:
- pug-code-gen "^2.0.2"
- pug-filters "^3.1.1"
- pug-lexer "^4.1.0"
- pug-linker "^3.0.6"
- pug-load "^2.0.12"
- pug-parser "^5.0.1"
- pug-runtime "^2.0.5"
- pug-strip-comments "^1.0.4"
+ pug-code-gen "^3.0.0"
+ pug-filters "^4.0.0"
+ pug-lexer "^5.0.0"
+ pug-linker "^4.0.0"
+ pug-load "^3.0.0"
+ pug-parser "^6.0.0"
+ pug-runtime "^3.0.0"
+ pug-strip-comments "^2.0.0"
pump@^2.0.0:
version "2.0.1"
@@ -11678,12 +11576,12 @@ readdirp@~3.4.0:
dependencies:
picomatch "^2.2.1"
-recast@^0.17.3:
- version "0.17.6"
- resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.6.tgz#64ae98d0d2dfb10ff92ff5fb9ffb7371823b69fa"
- integrity sha512-yoQRMRrK1lszNtbkGyM4kN45AwylV5hMiuEveUBlxytUViWevjvX6w+tzJt1LH4cfUhWt4NZvy3ThIhu6+m5wQ==
+recast@0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.19.1.tgz#555f3612a5a10c9f44b9a923875c51ff775de6c8"
+ integrity sha512-8FCjrBxjeEU2O6I+2hyHyBFH1siJbMBLwIRvVr1T3FD2cL754sOaJDsJ/8h3xYltasbJ8jqWRIhMuDGBSiSbjw==
dependencies:
- ast-types "0.12.4"
+ ast-types "0.13.3"
esprima "~4.0.0"
private "^0.1.8"
source-map "~0.6.1"
@@ -11741,9 +11639,9 @@ regenerator-runtime@^0.12.0:
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.4:
- version "0.13.5"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
- integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
regenerator-transform@^0.14.2:
version "0.14.5"
@@ -11804,9 +11702,9 @@ registry-auth-token@^3.0.1:
safe-buffer "^5.0.1"
registry-auth-token@^4.0.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479"
- integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
+ integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==
dependencies:
rc "^1.2.8"
@@ -11917,7 +11815,7 @@ repeat-element@^1.1.2:
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
-repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1:
+repeat-string@^1.5.4, repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
@@ -11946,29 +11844,29 @@ request-progress@3.0.0:
dependencies:
throttleit "^1.0.0"
-request-promise-core@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
- integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+request-promise-core@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+ integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
dependencies:
- lodash "^4.17.15"
+ lodash "^4.17.19"
request-promise-native@^1.0.7:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
- integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+ integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
dependencies:
- request-promise-core "1.1.3"
+ request-promise-core "1.1.4"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request-promise@^4.1.1:
- version "4.2.5"
- resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c"
- integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2"
+ integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==
dependencies:
bluebird "^3.5.0"
- request-promise-core "1.1.3"
+ request-promise-core "1.1.4"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
@@ -12079,7 +11977,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1:
+resolve@^1.10.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1:
version "1.17.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
@@ -12147,13 +12045,6 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
-right-align@^0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
- integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8=
- dependencies:
- align-text "^0.1.1"
-
rimraf@2, rimraf@2.7.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -12243,7 +12134,7 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -12396,7 +12287,7 @@ sentence-case@^2.1.0:
no-case "^2.2.0"
upper-case-first "^1.1.2"
-serialize-javascript@^2.1.0, serialize-javascript@^2.1.2:
+serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
@@ -12408,6 +12299,13 @@ serialize-javascript@^3.1.0:
dependencies:
randombytes "^2.1.0"
+serialize-javascript@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
+ integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
+ dependencies:
+ randombytes "^2.1.0"
+
serializerr@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91"
@@ -12693,7 +12591,7 @@ source-map@^0.4.2:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.1:
+source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -13071,9 +12969,9 @@ strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
strip-json-comments@^3.0.1:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
- integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-loader@^1.0.0:
version "1.2.1"
@@ -13405,68 +13303,68 @@ tiny-warning@^1.0.2:
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tippy.js@^6.2.3:
- version "6.2.4"
- resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.4.tgz#b76000080cc035745cab4f20ac7a30e1ccb1b6bb"
- integrity sha512-S3qLJhx7cpeGDpHw411jU62W1RvOGPkt3r68y8nwPi7wm/aexrSYAADbVb1ZNYCjspEwLWQvtAx76COvPcLvCw==
+ version "6.2.6"
+ resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.6.tgz#4991bbe8f75e741fb92b5ccfeebcd072d71f8345"
+ integrity sha512-0tTL3WQNT0nWmpslhDryRahoBm6PT9fh1xXyDfOsvZpDzq52by2rF2nvsW0WX2j9nUZP/jSGDqfKJGjCtoGFKg==
dependencies:
- "@popperjs/core" "^2.3.2"
+ "@popperjs/core" "^2.4.4"
-tiptap-commands@^1.13.1:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.13.1.tgz#b605292aa333192f33c7c50079c96262cdc7ff2a"
- integrity sha512-bniLSrnxId9zlcuwo4lVA4cmUgHpDzDhz5yw6ubDP0O++xtHl96me5E3lje8VAgBbMvP1txwwu6xSUp8xrAuXQ==
+tiptap-commands@^1.14.3:
+ version "1.14.3"
+ resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.14.3.tgz#84d6fbecaa642cd2ef44d6024569c211866b8567"
+ integrity sha512-4Nvs3vDf1UM5bvhko/7yQH3gN0QePWqJn4Q34N5t1sabwfF7bObyn/lIvQZvbUP3M0N1pUcWqzm6bOXlQ38yCQ==
dependencies:
- prosemirror-commands "1.1.4"
- prosemirror-inputrules "1.1.2"
- prosemirror-model "1.9.1"
- prosemirror-schema-list "1.1.2"
- prosemirror-state "1.3.3"
- prosemirror-tables "1.0.0"
- prosemirror-utils "0.9.6"
- tiptap-utils "^1.9.1"
+ prosemirror-commands "^1.1.4"
+ prosemirror-inputrules "^1.1.2"
+ prosemirror-model "^1.10.0"
+ prosemirror-schema-list "^1.1.2"
+ prosemirror-state "^1.3.3"
+ prosemirror-tables "^1.1.0"
+ prosemirror-utils "^0.9.6"
+ tiptap-utils "^1.10.3"
tiptap-extensions@^1.29.1:
- version "1.29.1"
- resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.29.1.tgz#70679bd57ffcc6a67358f58d03e574b40e0432bb"
- integrity sha512-xwBvlGAN0W9+F5DB/s/pH8LcOaUq7WgPffv7KOGU26jmPKq8JAXXwZXn8DOmPYaRo9RscF0Tg9AADOM+0vcLkQ==
+ version "1.31.3"
+ resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.31.3.tgz#bebaaee69e87f6f85f5d0ab17f84bc48a1f6c767"
+ integrity sha512-brS9ptnFxqJuE9MpwUP0LucptvhPcIhDkWNMftnWplbHOml5xEL6zR40FYkmmCq+ZJRh6+kS5HI9HYRp3StXMw==
dependencies:
- lowlight "1.13.1"
- prosemirror-collab "1.2.2"
- prosemirror-history "1.1.3"
- prosemirror-model "1.9.1"
- prosemirror-state "1.3.3"
- prosemirror-tables "1.0.0"
- prosemirror-transform "1.2.5"
- prosemirror-utils "0.9.6"
- prosemirror-view "1.14.7"
- tiptap "^1.27.1"
- tiptap-commands "^1.13.1"
+ lowlight "^1.14.0"
+ prosemirror-collab "^1.2.2"
+ prosemirror-history "^1.1.3"
+ prosemirror-model "^1.10.0"
+ prosemirror-state "^1.3.3"
+ prosemirror-tables "^1.1.0"
+ prosemirror-transform "^1.2.6"
+ prosemirror-utils "^0.9.6"
+ prosemirror-view "^1.15.0"
+ tiptap "^1.29.3"
+ tiptap-commands "^1.14.3"
-tiptap-utils@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.9.1.tgz#1adf749ecf417b3db4972afb7fa55dcef1329382"
- integrity sha512-E0tRFTNRYYwFRBhmSEjOUFmMnEyUD5rZ2QjiJaxf4ZXAPiUVy3gt2J7DqBjeP1q1FsmXkkkAHsxV+5hqX/lfFg==
+tiptap-utils@^1.10.3:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.10.3.tgz#3b902530452a3522d5c3f2f49833b13103f9703b"
+ integrity sha512-UmEOTPgUeS9qali1wOXY5gqhxNjZ5vDe7vGUZ99oPN1LsavEGx4kg+O0OqerlLkQv0gcRG88BMZouhhNzg87Zw==
dependencies:
- prosemirror-model "1.9.1"
- prosemirror-state "1.3.3"
- prosemirror-tables "1.0.0"
- prosemirror-utils "0.9.6"
+ prosemirror-model "^1.10.0"
+ prosemirror-state "^1.3.3"
+ prosemirror-tables "^1.1.0"
+ prosemirror-utils "^0.9.6"
-tiptap@^1.26.0, tiptap@^1.27.1:
- version "1.27.1"
- resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.27.1.tgz#b40b6634f23913b4570d6e7a8ed4eb5b206c7589"
- integrity sha512-CwPMwKAKjAzsnkxZSISqDh73JmTZP3qpYn91k71WfIUZ7KUDkDt8gOKDrHMhaTJR2qMmuAChkkzd3OvBaBX+/Q==
+tiptap@^1.26.0, tiptap@^1.29.3:
+ version "1.29.3"
+ resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.29.3.tgz#690dee9bcfc38d1f8d1c484de8d4d0314ceae996"
+ integrity sha512-1dngx+UsySlnfGHu8vDQpcrXcROzMMsFPGJrxSsf0/79dUgIdIIW01ZM8j0cIPKm+7BdXguhMRCxyN8u3NvgWA==
dependencies:
prosemirror-commands "1.1.4"
prosemirror-dropcursor "1.3.2"
prosemirror-gapcursor "1.1.5"
prosemirror-inputrules "1.1.2"
- prosemirror-keymap "1.1.3"
- prosemirror-model "1.9.1"
+ prosemirror-keymap "1.1.4"
+ prosemirror-model "1.10.0"
prosemirror-state "1.3.3"
- prosemirror-view "1.14.7"
- tiptap-commands "^1.13.1"
- tiptap-utils "^1.9.1"
+ prosemirror-view "1.15.0"
+ tiptap-commands "^1.14.3"
+ tiptap-utils "^1.10.3"
title-case@^2.1.0:
version "2.1.1"
@@ -13518,11 +13416,6 @@ to-ast@^1.0.0:
ast-types "^0.7.2"
esprima "^2.1.0"
-to-fast-properties@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
- integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
-
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -13570,10 +13463,10 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
-token-stream@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a"
- integrity sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=
+token-stream@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
+ integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
toposort@^1.0.0:
version "1.0.7"
@@ -13803,9 +13696,9 @@ typedarray@^0.0.6:
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.9.3, typescript@~3.9.3:
- version "3.9.6"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
- integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
+ version "3.9.7"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
+ integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
uglify-js@3.4.x:
version "3.4.10"
@@ -13815,21 +13708,6 @@ uglify-js@3.4.x:
commander "~2.19.0"
source-map "~0.6.1"
-uglify-js@^2.6.1:
- version "2.8.29"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
- integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0=
- dependencies:
- source-map "~0.5.1"
- yargs "~3.10.0"
- optionalDependencies:
- uglify-to-browserify "~1.0.0"
-
-uglify-to-browserify@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
- integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc=
-
unherit@^1.0.4:
version "1.1.3"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
@@ -13950,9 +13828,9 @@ unist-util-visit-parents@^2.0.0:
unist-util-is "^3.0.0"
unist-util-visit-parents@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz#d4076af3011739c71d2ce99d05de37d545f4351d"
- integrity sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g==
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5"
+ integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
@@ -13965,9 +13843,9 @@ unist-util-visit@^1.1.0:
unist-util-visit-parents "^2.0.0"
unist-util-visit@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.2.tgz#3843782a517de3d2357b4c193b24af2d9366afb7"
- integrity sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
+ integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
@@ -14244,9 +14122,9 @@ vfile-message@^2.0.0:
unist-util-stringify-position "^2.0.0"
vfile@^4.0.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.1.1.tgz#282d28cebb609183ac51703001bc18b3e3f17de9"
- integrity sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01"
+ integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==
dependencies:
"@types/unist" "^2.0.0"
is-buffer "^2.0.0"
@@ -14259,31 +14137,31 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
-void-elements@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
- integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+void-elements@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
+ integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
vue-apollo@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.3.tgz#7f29558df76eec0f03251847eef153816a261827"
- integrity sha512-WJaQ1v/i46/oIPlKv7J0Tx6tTlbuaeCdhrAbL06h+Zca2gzr5ywjUFpl8ijMTGJsQ+Ph/U4xEpBFBOMxQmL+7g==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.4.tgz#d48a990d02c1febb5248d097f921c74c50a22e8a"
+ integrity sha512-sthSS9E6FB7OMmSJmIG7e89QZvzwK/1PCD8A/IfGBST48pxY7sdSxRp22Gu2/s/gxQBnQPI1H1ZPZE97IG+zXA==
dependencies:
chalk "^2.4.2"
- serialize-javascript "^2.1.0"
+ serialize-javascript "^4.0.0"
throttle-debounce "^2.1.0"
vue-class-component@^7.2.3:
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.3.tgz#a5b1abd53513a72ad51098752e2dedd499807cca"
- integrity sha512-oEqYpXKaFN+TaXU+mRLEx8dX0ah85aAJEe61mpdoUrq0Bhe/6sWhyZX1JjMQLhVsHAkncyhedhmCdDVSasUtDw==
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.5.tgz#212b3548c4fdd3314774c4adbc1c3792a40b52d0"
+ integrity sha512-0CSftHY0bDTD+4FbYkuFf6+iKDjZ4h2in2YYJDRMk5daZIjrgT9LjFHvP7Rzqy9/s1pij3zDtTSLRUjsPWMwqg==
-vue-cli-plugin-styleguidist@~4.26.0:
- version "4.26.0"
- resolved "https://registry.yarnpkg.com/vue-cli-plugin-styleguidist/-/vue-cli-plugin-styleguidist-4.26.0.tgz#fd267f763e85939756b42db0e1a902343113db0b"
- integrity sha512-cyS6cGwEG+wghZO8T06O6A2sUMMPpoVIbqQLMWqZTc+zU+A56+Ljo6ecPN0L9G0yaOv9MKXuIPZrAahLNGyxBQ==
+vue-cli-plugin-styleguidist@~4.29.1:
+ version "4.29.1"
+ resolved "https://registry.yarnpkg.com/vue-cli-plugin-styleguidist/-/vue-cli-plugin-styleguidist-4.29.1.tgz#da4c1657e60dde359501cc880d638fd3efb8d9ad"
+ integrity sha512-POP+8kNKweWPZKd9/PuNt082WRLFO9gLKL9dGq2C0+yOr7jh9jejSknU5rDgk6vdyzIkTkgZfbg19FotmAue4Q==
dependencies:
- vue-styleguidist "^4.26.0"
+ vue-styleguidist "^4.29.1"
webpack-merge "^4.2.1"
vue-cli-plugin-svg@~0.1.3:
@@ -14297,18 +14175,18 @@ vue-cli-plugin-svg@~0.1.3:
url-loader "^2.0.0"
vue-svg-loader "^0.12.0"
-vue-docgen-api@^4.26.0:
- version "4.26.0"
- resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.26.0.tgz#bf1a7fd201ddbcd62e4432a0e8b6369651fcf1fe"
- integrity sha512-uJbmLup5NHukMUecMJiKgjLPdPIFDjlCFwOGX107S5gdlb+c/rd4ihOvWRAuVEBiUXWFxHJ12DZxCX1/UJGPcQ==
+vue-docgen-api@^4.29.1:
+ version "4.29.1"
+ resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.29.1.tgz#ee744d5636c3050d78b4f8ef5f49ca91c99bf869"
+ integrity sha512-DSN7d94shRr2p7YdfooMi+Wa8jP4QfA96REV+5zMLr3/cEu8rf6gXNS8FuR2+iv6hAeUsQfH4cmq/15oje+BDw==
dependencies:
"@babel/parser" "^7.6.0"
"@babel/types" "^7.6.0"
- ast-types "^0.12.2"
+ ast-types "0.13.3"
hash-sum "^1.0.2"
lru-cache "^4.1.5"
- pug "^2.0.3"
- recast "^0.17.3"
+ pug "^3.0.0"
+ recast "0.19.1"
ts-map "^1.0.3"
vue-template-compiler "^2.0.0"
@@ -14342,27 +14220,27 @@ vue-i18n-extract@^1.0.2:
js-yaml "^3.13.1"
vue-i18n@^8.14.0:
- version "8.18.2"
- resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.18.2.tgz#cd7c12f2e178e6faa23b0e3cfd2f7bac9305f8fc"
- integrity sha512-0X5nBTCZAVjlwcrPaYJwNs3iipBBTv0AUHwQUOa8yP3XbQGWKbRHqBb3OhCYtum/IHDD21d/df5Xd2VgyxbxfA==
+ version "8.20.0"
+ resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.20.0.tgz#c81b01d6541182b28565316cafe881b65a3c0f1b"
+ integrity sha512-ZiAOoeR4d/JtKpbjipx3I80ey7cYG1ki5gQ7HwzWm4YFio9brA15BEYHjalEoBaEfzF5OBEZP+Y2MvAaWnyXXg==
-vue-inbrowser-compiler-utils@^4.23.3:
- version "4.23.3"
- resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.23.3.tgz#188f1a439d0b7776f4a4255076105239e0f31ee2"
- integrity sha512-//NgjTyi9E2zEbVaXnkOHqKEpuCQ67HrEfvfLjBeuy9VOy07jH26FbdBG/bL3zA/iwXiHdiePSa1+YdvSyCKpA==
+vue-inbrowser-compiler-utils@^4.27.0:
+ version "4.27.0"
+ resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.27.0.tgz#06f07b8fe9d2422a9ea652a508a7a77250430b56"
+ integrity sha512-NnsvgVQ0piyHO89RNjqyE/VrvFApItR5wHwYXjXwYVKSG6sx6ZyzAYhdW7JYC0rFoBz+kuTxdddDBDynnMEsCA==
dependencies:
camelcase "^5.3.1"
-vue-inbrowser-compiler@^4.23.3:
- version "4.23.3"
- resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.23.3.tgz#2992366dc14cb7efca74fce8a410307eb662b544"
- integrity sha512-ac5wMcvH0dpSeE2TsO7dY9zkyJ9UBB6R7U0IMsyWf6j1xSeHavCFtuOOQwphobLYlPqvneK1DoMwhVkDVm/fkQ==
+vue-inbrowser-compiler@^4.27.0:
+ version "4.27.0"
+ resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.27.0.tgz#468be2c948b1df8a451db49d02f7e4d92b918c1d"
+ integrity sha512-P57562nGgzlF5YY53XKCtgDtjGwFVEzUyOomilzMIluhg9LGHeMgSlQZcWcSI39xyTrEPKwJQLTvTUOHXLjVZg==
dependencies:
acorn "^6.1.1"
acorn-jsx "^5.0.1"
buble "^0.19.7"
camelcase "^5.3.1"
- vue-inbrowser-compiler-utils "^4.23.3"
+ vue-inbrowser-compiler-utils "^4.27.0"
walkes "^0.2.1"
vue-loader@^15.9.2:
@@ -14399,9 +14277,9 @@ vue-router@^3.1.6:
integrity sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg==
vue-scrollto@^2.17.1:
- version "2.18.1"
- resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.18.1.tgz#e23620489583f0b3f21410f149b6585a730672f2"
- integrity sha512-JojeVOP1Z50Jt0OCOd61jRRRF8TrNvPAzF4LAKwkTEZnt30mpDd48aDZZOEPD3l6z2PybOyUGFT/FNWfTtFv9w==
+ version "2.18.2"
+ resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.18.2.tgz#113a3548341974d3a2560145f39fcc0378533821"
+ integrity sha512-LQOsuCfDJ5JXsRBb6frM85sCFABZdkwwwchLh4k3wbp8P6URRDcQvwp31U4cTnbyYEOo6JFP7+3p0wkvwy7MdA==
dependencies:
bezier-easing "2.1.0"
@@ -14413,13 +14291,13 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.2:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-styleguidist@^4.26.0:
- version "4.26.0"
- resolved "https://registry.yarnpkg.com/vue-styleguidist/-/vue-styleguidist-4.26.0.tgz#2e0be5a5bbbb276e0cfad1daffe0594ff7c25b23"
- integrity sha512-/H/d47DMcCvSew88q3nXD+P5jlYvlBFqu162S9EqY4h3G1E1qaBXHxcKGAutJaEJNlWOWeNU8RrUmAau0Jb6Sw==
+vue-styleguidist@^4.29.1:
+ version "4.29.1"
+ resolved "https://registry.yarnpkg.com/vue-styleguidist/-/vue-styleguidist-4.29.1.tgz#8fd8a3000e0be83525e0441eb549c9dd7133037a"
+ integrity sha512-CqWk8+qcetLX5l4NYQImmSQkkWkOoEGj715JJWfgkr8VQDU1aY28KWzxnlj+Dn1+RUttP/a+gQXqxqtoJPhHbQ==
dependencies:
"@vxna/mini-html-webpack-template" "^1.0.0"
- ast-types "^0.13.2"
+ ast-types "^0.13.3"
classnames "^2.2.6"
clean-webpack-plugin "^3.0.0"
cli-progress "^3.0.0"
@@ -14466,9 +14344,9 @@ vue-styleguidist@^4.26.0:
style-loader "^1.0.0"
terser-webpack-plugin "^2.2.2"
to-ast "^1.0.0"
- vue-docgen-api "^4.26.0"
- vue-inbrowser-compiler "^4.23.3"
- vue-inbrowser-compiler-utils "^4.23.3"
+ vue-docgen-api "^4.29.1"
+ vue-inbrowser-compiler "^4.27.0"
+ vue-inbrowser-compiler-utils "^4.27.0"
webpack-dev-server "^3.11.0"
webpack-merge "^4.0.0"
@@ -14503,7 +14381,7 @@ vue@>=2.0.0, vue@^2.6.11:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
-vuedraggable@^2.23.2:
+vuedraggable@2.23.2:
version "2.23.2"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.2.tgz#0d95d7fdf4f02f56755a26b3c9dca5c7ca9cfa72"
integrity sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ==
@@ -14543,15 +14421,15 @@ watchpack-chokidar2@^2.0.0:
dependencies:
chokidar "^2.1.8"
-watchpack@^1.6.1:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa"
- integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==
+watchpack@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
+ integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
dependencies:
graceful-fs "^4.1.2"
neo-async "^2.5.0"
optionalDependencies:
- chokidar "^3.4.0"
+ chokidar "^3.4.1"
watchpack-chokidar2 "^2.0.0"
wbuf@^1.1.0, wbuf@^1.7.3:
@@ -14593,9 +14471,9 @@ webpack-bundle-analyzer@^3.8.0:
ws "^6.0.0"
webpack-chain@^6.4.0:
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.5.0.tgz#0b4af2094a5058a9ccd34b8f7ab194de4c83365f"
- integrity sha512-K4EHiEg4WlP4w1rKXKpYWvX9cfGBERHCGP06ETSNV62XUIfOUg1DDRQpxyBsFYxZLKc4YUAI3iiCIvWoliheGA==
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.5.1.tgz#4f27284cbbb637e3c8fbdef43eef588d4d861206"
+ integrity sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==
dependencies:
deepmerge "^1.5.2"
javascript-stringify "^2.0.1"
@@ -14691,9 +14569,9 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-
source-map "~0.6.1"
webpack@^4.0.0:
- version "4.43.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6"
- integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==
+ version "4.44.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21"
+ integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==
dependencies:
"@webassemblyjs/ast" "1.9.0"
"@webassemblyjs/helper-module-context" "1.9.0"
@@ -14703,7 +14581,7 @@ webpack@^4.0.0:
ajv "^6.10.2"
ajv-keywords "^3.4.1"
chrome-trace-event "^1.0.2"
- enhanced-resolve "^4.1.0"
+ enhanced-resolve "^4.3.0"
eslint-scope "^4.0.3"
json-parse-better-errors "^1.0.2"
loader-runner "^2.4.0"
@@ -14716,7 +14594,7 @@ webpack@^4.0.0:
schema-utils "^1.0.0"
tapable "^1.1.3"
terser-webpack-plugin "^1.4.3"
- watchpack "^1.6.1"
+ watchpack "^1.7.4"
webpack-sources "^1.4.1"
websocket-driver@0.6.5:
@@ -14753,9 +14631,9 @@ whatwg-fetch@2.0.4:
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
whatwg-fetch@>=0.10.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz#49d630cdfa308dba7f2819d49d09364f540dbcc6"
- integrity sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc"
+ integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w==
whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
version "2.3.0"
@@ -14804,29 +14682,21 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
-window-size@0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
- integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=
-
-with@^5.0.0:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/with/-/with-5.1.1.tgz#fa4daa92daf32c4ea94ed453c81f04686b575dfe"
- integrity sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=
+with@^7.0.0:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
+ integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
dependencies:
- acorn "^3.1.0"
- acorn-globals "^3.0.0"
+ "@babel/parser" "^7.9.6"
+ "@babel/types" "^7.9.6"
+ assert-never "^1.2.1"
+ babel-walk "3.0.0-canary-5"
word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-wordwrap@0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
- integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=
-
workbox-background-sync@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz#26821b9bf16e9e37fd1d640289edddc08afd1950"
@@ -15031,9 +14901,9 @@ ws@^6.0.0, ws@^6.2.1:
async-limiter "~1.0.0"
ws@^7.0.0:
- version "7.3.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
- integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
+ integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
xdg-basedir@^3.0.0:
version "3.0.0"
@@ -15050,13 +14920,6 @@ xmlchars@^2.1.1:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-xregexp@^4.2.4:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"
- integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==
- dependencies:
- "@babel/runtime-corejs3" "^7.8.3"
-
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -15209,12 +15072,12 @@ yargs@^10.0.3:
yargs-parser "^8.1.0"
yargs@^15.0.0:
- version "15.4.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.0.tgz#53949fb768309bac1843de9b17b80051e9805ec2"
- integrity sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw==
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
- decamelize "^3.2.0"
+ decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
@@ -15244,16 +15107,6 @@ yargs@^8.0.2:
y18n "^3.2.1"
yargs-parser "^7.0.0"
-yargs@~3.10.0:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
- integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=
- dependencies:
- camelcase "^1.0.2"
- cliui "^2.1.0"
- decamelize "^1.0.0"
- window-size "0.1.0"
-
yauzl@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex
index a71c9e26..a3b8c201 100644
--- a/lib/federation/activity_pub/activity_pub.ex
+++ b/lib/federation/activity_pub/activity_pub.ex
@@ -13,41 +13,38 @@ defmodule Mobilizon.Federation.ActivityPub do
alias Mobilizon.{
Actors,
Config,
- Conversations,
+ Discussions,
Events,
- Reports,
Resources,
Share,
- Todos,
Users
}
alias Mobilizon.Actors.{Actor, Follower, Member}
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
- alias Mobilizon.Reports.Report
- alias Mobilizon.Resources.Resource
- alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Tombstone
alias Mobilizon.Federation.ActivityPub.{
Activity,
Audience,
Federator,
+ Fetcher,
+ Preloader,
Relay,
Transmogrifier,
+ Types,
Visibility
}
+ alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable}
+
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
- alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Federation.WebFinger
- alias Mobilizon.GraphQL.API.Utils, as: APIUtils
- alias Mobilizon.Service.Formatter.HTML
alias Mobilizon.Service.Notifications.Scheduler
- alias Mobilizon.Service.RichMedia.Parser
+ alias Mobilizon.Storage.Page
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Admin, Mailer}
@@ -74,75 +71,44 @@ defmodule Mobilizon.Federation.ActivityPub do
Fetch an object from an URL, from our local database of events and comments, then eventually remote
"""
# TODO: Make database calls parallel
- @spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
- def fetch_object_from_url(url) do
+ @spec fetch_object_from_url(String.t(), Keyword.t()) ::
+ {:ok, struct()} | {:error, any()}
+ def fetch_object_from_url(url, options \\ []) do
Logger.info("Fetching object from url #{url}")
+ force_fetch = Keyword.get(options, :force, false)
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")},
- {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(url)},
- {:existing_comment, nil} <- {:existing_comment, Conversations.get_comment_from_url(url)},
- {:existing_resource, nil} <- {:existing_resource, Resources.get_resource_by_url(url)},
- {:existing_actor, {:error, :actor_not_found}} <-
- {:existing_actor, Actors.get_actor_by_url(url)},
- date <- Signature.generate_date_header(),
- headers <-
- [{:Accept, "application/activity+json"}]
- |> maybe_date_fetch(date)
- |> sign_fetch_relay(url, date),
- {:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 <-
- HTTPoison.get(
- url,
- headers,
- follow_redirect: true,
- timeout: 10_000,
- recv_timeout: 20_000,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ),
- {:ok, data} <- Jason.decode(body),
- {:origin_check, true} <- {:origin_check, origin_check?(url, data)},
- params <- %{
- "type" => "Create",
- "to" => data["to"],
- "cc" => data["cc"],
- "actor" => data["attributedTo"],
- "object" => data
- },
- {:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
- case data["type"] do
- "Event" ->
- {:ok, Events.get_public_event_by_url_with_preload!(object_url)}
-
- "Note" ->
- {:ok, Conversations.get_comment_from_url_with_preload!(object_url)}
-
- "Document" ->
- {:ok, Resources.get_resource_by_url_with_preloads(object_url)}
-
- "ResourceCollection" ->
- {:ok, Resources.get_resource_by_url_with_preloads(object_url)}
-
- "Actor" ->
- {:ok, Actors.get_actor_by_url!(object_url, true)}
-
- other ->
- {:error, other}
- end
+ {:existing, nil} <-
+ {:existing, Tombstone.find_tombstone(url)},
+ {:existing, nil} <- {:existing, Events.get_event_by_url(url)},
+ {:existing, nil} <-
+ {:existing, Discussions.get_discussion_by_url(url)},
+ {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
+ {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
+ {:existing, nil} <-
+ {:existing, Actors.get_actor_by_url_2(url)},
+ :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"),
+ {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do
+ Logger.debug("Going to preload the new entity")
+ Preloader.maybe_preload(entity)
else
- {:existing_event, %Event{url: event_url}} ->
- {:ok, Events.get_public_event_by_url_with_preload!(event_url)}
+ {:existing, entity} ->
+ Logger.debug("Entity is already existing")
- {:existing_comment, %Comment{url: comment_url}} ->
- {:ok, Conversations.get_comment_from_url_with_preload!(comment_url)}
+ entity =
+ if force_fetch and not compare_origins?(url, Endpoint.url()) do
+ Logger.debug("Entity is external and we want a force fetch")
- {:existing_resource, %Resource{url: resource_url}} ->
- {:ok, Resources.get_resource_by_url_with_preloads(resource_url)}
+ with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do
+ entity
+ end
+ else
+ entity
+ end
- {:existing_actor, {:ok, %Actor{url: actor_url}}} ->
- {:ok, Actors.get_actor_by_url!(actor_url, true)}
+ Logger.debug("Going to preload an existing entity")
- {:origin_check, false} ->
- Logger.warn("Object origin check failed")
- {:error, "Object origin check failed"}
+ Preloader.maybe_preload(entity)
e ->
Logger.warn("Something failed while fetching url #{inspect(e)}")
@@ -201,15 +167,18 @@ defmodule Mobilizon.Federation.ActivityPub do
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
{:ok, entity, create_data} <-
(case type do
- :event -> create_event(args, additional)
- :comment -> create_comment(args, additional)
- :group -> create_group(args, additional)
- :todo_list -> create_todo_list(args, additional)
- :todo -> create_todo(args, additional)
- :resource -> create_resource(args, additional)
+ :event -> Types.Events.create(args, additional)
+ :comment -> Types.Comments.create(args, additional)
+ :discussion -> Types.Discussions.create(args, additional)
+ :actor -> Types.Actors.create(args, additional)
+ :todo_list -> Types.TodoLists.create(args, additional)
+ :todo -> Types.Todos.create(args, additional)
+ :resource -> Types.Resources.create(args, additional)
+ :post -> Types.Posts.create(args, additional)
end),
{:ok, activity} <- create_activity(create_data, local),
- :ok <- maybe_federate(activity) do
+ :ok <- maybe_federate(activity),
+ :ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
@@ -227,21 +196,15 @@ defmodule Mobilizon.Federation.ActivityPub do
* Federates (asynchronously) the activity
* Returns the activity
"""
- @spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
- def update(type, old_entity, args, local \\ false, additional \\ %{}) do
+ @spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
+ def update(old_entity, args, local \\ false, additional \\ %{}) do
Logger.debug("updating an activity")
Logger.debug(inspect(args))
- with {:ok, entity, update_data} <-
- (case type do
- :event -> update_event(old_entity, args, additional)
- :comment -> update_comment(old_entity, args, additional)
- :actor -> update_actor(old_entity, args, additional)
- :todo -> update_todo(old_entity, args, additional)
- :resource -> update_resource(old_entity, args, additional)
- end),
+ with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional),
{:ok, activity} <- create_activity(update_data, local),
- :ok <- maybe_federate(activity) do
+ :ok <- maybe_federate(activity),
+ :ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
@@ -366,182 +329,48 @@ defmodule Mobilizon.Federation.ActivityPub do
end
end
- def delete(object, local \\ true)
-
- @spec delete(Event.t(), boolean) :: {:ok, Activity.t(), Event.t()}
- def delete(%Event{url: url, organizer_actor: actor} = event, local) do
- data = %{
- "type" => "Delete",
- "actor" => actor.url,
- "object" => url,
- "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
- "id" => url <> "/delete"
- }
-
- with audience <-
- Audience.calculate_to_and_cc_from_mentions(event),
- {:ok, %Event{} = event} <- Events.delete_event(event),
- {:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
- {:ok, %Tombstone{} = _tombstone} <-
- Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}),
- Share.delete_all_by_uri(event.url),
+ def delete(object, actor, local \\ true) do
+ with {:ok, activity_data, actor, object} <-
+ Managable.delete(object, actor, local),
+ group <- Ownable.group_actor(object),
:ok <- check_for_actor_key_rotation(actor),
- {:ok, activity} <- create_activity(Map.merge(data, audience), local),
- :ok <- maybe_federate(activity) do
- {:ok, activity, event}
+ {:ok, activity} <- create_activity(activity_data, local),
+ :ok <- maybe_federate(activity),
+ :ok <- maybe_relay_if_group_activity(activity, group) do
+ {:ok, activity, object}
end
end
- @spec delete(Comment.t(), boolean) :: {:ok, Activity.t(), Comment.t()}
- def delete(%Comment{url: url, actor: actor} = comment, local) do
- data = %{
- "type" => "Delete",
- "actor" => actor.url,
- "object" => url,
- "id" => url <> "/delete",
- "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
- }
-
- with audience <-
- Audience.calculate_to_and_cc_from_mentions(comment),
- {:ok, %Comment{} = comment} <- Conversations.delete_comment(comment),
- {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
- {:ok, %Tombstone{} = _tombstone} <-
- Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}),
- Share.delete_all_by_uri(comment.url),
- :ok <- check_for_actor_key_rotation(actor),
- {:ok, activity} <- create_activity(Map.merge(data, audience), local),
+ def join(%Event{} = event, %Actor{} = actor, local \\ true, additional \\ %{}) do
+ with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional),
+ {:ok, activity} <- create_activity(activity_data, local),
:ok <- maybe_federate(activity) do
- {:ok, activity, comment}
- end
- end
-
- def delete(%Actor{url: url} = actor, local) do
- data = %{
- "type" => "Delete",
- "actor" => url,
- "object" => url,
- "id" => url <> "/delete",
- "to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
- }
-
- # We completely delete the actor if activity is remote
- with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local),
- {:ok, activity} <- create_activity(data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity, actor}
- end
- end
-
- def delete(
- %Resource{url: url, actor: %Actor{url: actor_url}} = resource,
- local
- ) do
- Logger.debug("Building Delete Resource activity")
-
- data = %{
- "actor" => actor_url,
- "type" => "Delete",
- "object" => url,
- "id" => url <> "/delete",
- "to" => [actor_url]
- }
-
- Logger.debug(inspect(data))
-
- with {:ok, _resource} <- Resources.delete_resource(resource),
- {:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}"),
- {:ok, activity} <- create_activity(data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity, resource}
- end
- end
-
- def flag(args, local \\ false, _additional \\ %{}) do
- with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
- {:create_report, {:ok, %Report{} = report}} <-
- {:create_report, Reports.create_report(args)},
- report_as_data <- Convertible.model_to_as(report),
- cc <- if(local, do: [report.reported.url], else: []),
- report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}),
- {:ok, activity} <- create_activity(report_as_data, local),
- :ok <- maybe_federate(activity) do
- Enum.each(Users.list_moderators(), fn moderator ->
- moderator
- |> Admin.report(report)
- |> Mailer.deliver_later()
- end)
-
- {:ok, activity, report}
+ {:ok, activity, participant}
else
- err ->
- Logger.error("Something went wrong while creating an activity")
- Logger.debug(inspect(err))
- err
+ {:maximum_attendee_capacity, err} ->
+ {:maximum_attendee_capacity, err}
+
+ {:accept, accept} ->
+ accept
end
end
- def join(object, actor, local \\ true, additional \\ %{})
-
- def join(%Event{} = event, %Actor{} = actor, local, additional) do
- # TODO Refactor me for federation
- with {:maximum_attendee_capacity, true} <-
- {:maximum_attendee_capacity, check_attendee_capacity(event)},
- role <-
- additional
- |> Map.get(:metadata, %{})
- |> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
- {:ok, %Participant{} = participant} <-
- Mobilizon.Events.create_participant(%{
- role: role,
- event_id: event.id,
- actor_id: actor.id,
- url: Map.get(additional, :url),
- metadata:
- additional
- |> Map.get(:metadata, %{})
- |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
+ def join_group(
+ %{parent_id: parent_id, actor_id: actor_id, role: role},
+ local \\ true,
+ additional \\ %{}
+ ) do
+ with {:ok, %Member{} = member} <-
+ Mobilizon.Actors.create_member(%{
+ parent_id: parent_id,
+ actor_id: actor_id,
+ role: role
}),
- join_data <- Convertible.model_to_as(participant),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(participant),
- {:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
+ activity_data when is_map(activity_data) <-
+ Convertible.model_to_as(member),
+ {:ok, activity} <- create_activity(Map.merge(activity_data, additional), local),
:ok <- maybe_federate(activity) do
- if event.local do
- cond do
- Mobilizon.Events.get_default_participant_role(event) === :participant &&
- role == :participant ->
- accept(
- :join,
- participant,
- true,
- %{"actor" => event.organizer_actor.url}
- )
-
- Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
- role == :not_approved ->
- Scheduler.pending_participation_notification(event)
- {:ok, activity, participant}
-
- true ->
- {:ok, activity, participant}
- end
- else
- {:ok, activity, participant}
- end
- end
- end
-
- # TODO: Implement me
- def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local, _additional) do
- :error
- end
-
- defp check_attendee_capacity(%Event{options: options} = event) do
- with maximum_attendee_capacity <-
- Map.get(options, :maximum_attendee_capacity) || 0 do
- maximum_attendee_capacity == 0 ||
- Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
+ {:ok, activity, member}
end
end
@@ -640,7 +469,7 @@ defmodule Mobilizon.Federation.ActivityPub do
with {:ok, entity, update_data} <-
(case type do
- :resource -> move_resource(old_entity, args, additional)
+ :resource -> Types.Resources.move(old_entity, args, additional)
end),
{:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
@@ -653,6 +482,25 @@ defmodule Mobilizon.Federation.ActivityPub do
end
end
+ def flag(args, local \\ false, additional \\ %{}) do
+ with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
+ {:ok, activity} <- create_activity(report_as_data, local),
+ :ok <- maybe_federate(activity) do
+ Enum.each(Users.list_moderators(), fn moderator ->
+ moderator
+ |> Admin.report(report)
+ |> Mailer.deliver_later()
+ end)
+
+ {:ok, activity, report}
+ else
+ err ->
+ Logger.error("Something went wrong while creating an activity")
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
@doc """
Create an actor locally by its URL (AP ID)
"""
@@ -711,9 +559,29 @@ defmodule Mobilizon.Federation.ActivityPub do
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp is_create_activity?(_), do: false
- @spec is_announce_activity?(Activity.t()) :: boolean
- defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
- defp is_announce_activity?(_), do: false
+ @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
+ defp convert_members_in_recipients(recipients) do
+ Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
+ case Actors.get_group_by_members_url(recipient) do
+ # If the group is local just add external members
+ %Actor{domain: domain} = group when is_nil(domain) ->
+ {Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
+ member_actors ++ Actors.list_external_actors_members_for_group(group)}
+
+ # If it's remote add the remote group actor as well
+ %Actor{} = group ->
+ {Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
+ member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
+
+ _ ->
+ acc
+ end
+ end)
+ end
+
+ # @spec is_announce_activity?(Activity.t()) :: boolean
+ # defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
+ # defp is_announce_activity?(_), do: false
@doc """
Publish an activity to all appropriated audiences inboxes
@@ -741,19 +609,11 @@ defmodule Mobilizon.Federation.ActivityPub do
{recipients, []}
end
- # If we want to send to all members of the group, because this server is the one the group is on
- {recipients, members} =
- if is_announce_activity?(activity) and actor.type == :Group and
- actor.members_url in activity.recipients and is_nil(actor.domain) do
- {Enum.filter(recipients, fn recipient -> recipient != actor.members_url end),
- Actors.list_external_members_for_group(actor)}
- else
- {recipients, []}
- end
+ {recipients, members} = convert_members_in_recipients(recipients)
remote_inboxes =
(remote_actors(recipients) ++ followers ++ members)
- |> Enum.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end)
+ |> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
@@ -791,16 +651,15 @@ defmodule Mobilizon.Federation.ActivityPub do
date: date
})
- HTTPoison.post(
+ Tesla.post(
inbox,
json,
- [
+ headers: [
{"Content-Type", "application/activity+json"},
{"signature", signature},
{"digest", digest},
{"date", date}
- ],
- hackney: [pool: :default]
+ ]
)
end
@@ -811,18 +670,15 @@ defmodule Mobilizon.Federation.ActivityPub do
Logger.debug(inspect(url))
res =
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, [Accept: "application/activity+json"],
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ),
+ with {:ok, %{status: 200, body: body}} <-
+ Tesla.get(url, headers: [{"Accept", "application/activity+json"}]),
:ok <- Logger.debug("response okay, now decoding json"),
{:ok, data} <- Jason.decode(body) do
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
{:ok, Converter.Actor.as_to_model_data(data)}
else
# Actor is gone, probably deleted
- {:ok, %HTTPoison.Response{status_code: 410}} ->
+ {:ok, %{status: 410}} ->
Logger.info("Response HTTP 410")
{:error, :actor_deleted}
@@ -839,10 +695,11 @@ defmodule Mobilizon.Federation.ActivityPub do
"""
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
- {:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
+ %Page{total: total_events, elements: events} =
+ Events.list_public_events_for_actor(actor, page, limit)
- {:ok, comments, total_comments} =
- Conversations.list_public_comments_for_actor(actor, page, limit)
+ %Page{total: total_comments, elements: comments} =
+ Discussions.list_public_comments_for_actor(actor, page, limit)
event_activities = Enum.map(events, &event_to_activity/1)
comment_activities = Enum.map(comments, &comment_to_activity/1)
@@ -879,252 +736,10 @@ defmodule Mobilizon.Federation.ActivityPub do
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
end
- @spec create_event(map(), map()) :: {:ok, map()}
- defp create_event(args, additional) do
- with args <- prepare_args_for_event(args),
- {:ok, %Event{} = event} <- Events.create_event(args),
- event_as_data <- Convertible.model_to_as(event),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(event),
- create_data <-
- make_create_data(event_as_data, Map.merge(audience, additional)) do
- {:ok, event, create_data}
- end
- end
-
- @spec create_comment(map(), map()) :: {:ok, map()}
- defp create_comment(args, additional) do
- with args <- prepare_args_for_comment(args),
- {:ok, %Comment{} = comment} <- Conversations.create_comment(args),
- comment_as_data <- Convertible.model_to_as(comment),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(comment),
- create_data <-
- make_create_data(comment_as_data, Map.merge(audience, additional)) do
- {:ok, comment, create_data}
- end
- end
-
- @spec create_group(map(), map()) :: {:ok, map()}
- defp create_group(args, additional) do
- with args <- prepare_args_for_group(args),
- {:ok, %Actor{type: :Group} = group} <- Actors.create_group(args),
- group_as_data <- Convertible.model_to_as(group),
- audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
- create_data <-
- make_create_data(group_as_data, Map.merge(audience, additional)) do
- {:ok, group, create_data}
- end
- end
-
- @spec create_todo_list(map(), map()) :: {:ok, map()}
- defp create_todo_list(args, additional) do
- with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
- audience <- %{"to" => [group.url], "cc" => []},
- create_data <-
- make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
- {:ok, todo_list, create_data}
- end
- end
-
- @spec create_todo(map(), map()) :: {:ok, map()}
- defp create_todo(args, additional) do
- with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
- Todos.create_todo(args),
- %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
- %Actor{} = creator <- Actors.get_actor(creator_id),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
- todo_as_data <-
- Convertible.model_to_as(todo),
- audience <- %{"to" => [group.url], "cc" => []},
- create_data <-
- make_create_data(todo_as_data, Map.merge(audience, additional)) do
- {:ok, todo, create_data}
- end
- end
-
- defp create_resource(%{type: type} = args, additional) do
- args =
- case type do
- :folder ->
- args
-
- _ ->
- case Parser.parse(Map.get(args, :resource_url)) do
- {:ok, metadata} ->
- Map.put(args, :metadata, metadata)
-
- _ ->
- args
- end
- end
-
- with {:ok,
- %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
- Resources.create_resource(args),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
- resource_as_data <-
- Convertible.model_to_as(%{resource | actor: group, creator: creator}),
- audience <- %{
- "to" => [group.url],
- "cc" => [],
- "actor" => creator_url,
- "attributedTo" => [creator_url]
- } do
- create_data =
- case parent_id do
- nil ->
- make_create_data(resource_as_data, Map.merge(audience, additional))
-
- parent_id ->
- # In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
- parent = Resources.get_resource(parent_id)
- make_add_data(resource_as_data, parent, Map.merge(audience, additional))
- end
-
- {:ok, resource, create_data}
- else
- err ->
- Logger.error(inspect(err))
- err
- end
- end
-
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
defp check_for_tombstones(_), do: nil
- @spec update_event(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
- defp update_event(%Event{} = old_event, args, additional) do
- with args <- prepare_args_for_event(args),
- {:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
- {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
- event_as_data <- Convertible.model_to_as(new_event),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(new_event),
- update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
- {:ok, new_event, update_data}
- else
- err ->
- Logger.error("Something went wrong while creating an update activity")
- Logger.debug(inspect(err))
- err
- end
- end
-
- @spec update_comment(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
- defp update_comment(%Comment{} = old_comment, args, additional) do
- with args <- prepare_args_for_comment(args),
- {:ok, %Comment{} = new_comment} <- Conversations.update_comment(old_comment, args),
- {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
- comment_as_data <- Convertible.model_to_as(new_comment),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(new_comment),
- update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
- {:ok, new_comment, update_data}
- else
- err ->
- Logger.error("Something went wrong while creating an update activity")
- Logger.debug(inspect(err))
- err
- end
- end
-
- @spec update_actor(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
- defp update_actor(%Actor{} = old_actor, args, additional) do
- with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
- actor_as_data <- Convertible.model_to_as(new_actor),
- {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
- audience <-
- Audience.calculate_to_and_cc_from_mentions(new_actor),
- additional <- Map.merge(additional, %{"actor" => old_actor.url}),
- update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
- {:ok, new_actor, update_data}
- end
- end
-
- @spec update_todo(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
- defp update_todo(%Todo{} = old_todo, args, additional) do
- with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
- %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- todo_as_data <-
- Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
- audience <- %{"to" => [group.url], "cc" => []},
- update_data <-
- make_update_data(todo_as_data, Map.merge(audience, additional)) do
- {:ok, todo, update_data}
- end
- end
-
- defp update_resource(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
- move_resource(old_resource, args, additional)
- end
-
- # Simple rename
- defp update_resource(%Resource{} = old_resource, %{title: title} = _args, additional) do
- with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
- Resources.update_resource(old_resource, %{title: title}),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- %Actor{url: creator_url} <- Actors.get_actor(creator_id),
- resource_as_data <-
- Convertible.model_to_as(%{resource | actor: group}),
- audience <- %{
- "to" => [group.url],
- "cc" => [],
- "actor" => creator_url,
- "attributedTo" => [creator_url]
- },
- update_data <-
- make_update_data(resource_as_data, Map.merge(audience, additional)) do
- {:ok, resource, update_data}
- else
- err ->
- Logger.error(inspect(err))
- err
- end
- end
-
- defp move_resource(
- %Resource{parent_id: old_parent_id} = old_resource,
- %{parent_id: _new_parent_id} = args,
- additional
- ) do
- with {:ok,
- %Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
- resource} <-
- Resources.update_resource(old_resource, args),
- old_parent <- Resources.get_resource(old_parent_id),
- new_parent <- Resources.get_resource(new_parent_id),
- {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
- %Actor{url: creator_url} <- Actors.get_actor(creator_id),
- resource_as_data <-
- Convertible.model_to_as(%{resource | actor: group}),
- audience <- %{
- "to" => [group.url],
- "cc" => [],
- "actor" => creator_url,
- "attributedTo" => [creator_url]
- },
- move_data <-
- make_move_data(
- resource_as_data,
- old_parent,
- new_parent,
- Map.merge(audience, additional)
- ) do
- {:ok, resource, move_data}
- else
- err ->
- Logger.error(inspect(err))
- err
- end
- end
-
@spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any
defp accept_follow(%Follower{} = follower, additional) do
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
@@ -1254,138 +869,4 @@ defmodule Mobilizon.Federation.ActivityPub do
err
end
end
-
- # Prepare and sanitize arguments for events
- defp prepare_args_for_event(args) do
- # If title is not set: we are not updating it
- args =
- if Map.has_key?(args, :title) && !is_nil(args.title),
- do: Map.update(args, :title, "", &String.trim/1),
- else: args
-
- # If we've been given a description (we might not get one if updating)
- # sanitize it, HTML it, and extract tags & mentions from it
- args =
- if Map.has_key?(args, :description) && !is_nil(args.description) do
- {description, mentions, tags} =
- APIUtils.make_content_html(
- String.trim(args.description),
- Map.get(args, :tags, []),
- "text/html"
- )
-
- mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
-
- Map.merge(args, %{
- description: description,
- mentions: mentions,
- tags: tags
- })
- else
- args
- end
-
- # Check that we can only allow anonymous participation if our instance allows it
- {_, options} =
- Map.get_and_update(
- Map.get(args, :options, %{anonymous_participation: false}),
- :anonymous_participation,
- fn value ->
- {value, value && Mobilizon.Config.anonymous_participation?()}
- end
- )
-
- args = Map.put(args, :options, options)
-
- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
- end
-
- # Prepare and sanitize arguments for comments
- defp prepare_args_for_comment(args) do
- with in_reply_to_comment <-
- args |> Map.get(:in_reply_to_comment_id) |> Conversations.get_comment_with_preload(),
- event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
- args <- Map.update(args, :visibility, :public, & &1),
- {text, mentions, tags} <-
- APIUtils.make_content_html(
- args |> Map.get(:text, "") |> String.trim(),
- # Can't put additional tags on a comment
- [],
- "text/html"
- ),
- tags <- ConverterUtils.fetch_tags(tags),
- mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
- args <-
- Map.merge(args, %{
- actor_id: Map.get(args, :actor_id),
- text: text,
- mentions: mentions,
- tags: tags,
- event: event,
- in_reply_to_comment: in_reply_to_comment,
- in_reply_to_comment_id:
- if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
- origin_comment_id:
- if(is_nil(in_reply_to_comment),
- do: nil,
- else: Comment.get_thread_id(in_reply_to_comment)
- )
- }) do
- args
- end
- end
-
- @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
- defp handle_event_for_comment(event_id) when not is_nil(event_id) do
- case Events.get_event_with_preload(event_id) do
- {:ok, %Event{} = event} -> event
- {:error, :event_not_found} -> nil
- end
- end
-
- defp handle_event_for_comment(nil), do: nil
-
- defp prepare_args_for_group(args) do
- with preferred_username <-
- args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
- summary <- args |> Map.get(:summary, "") |> String.trim(),
- {summary, _mentions, _tags} <-
- summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
- %{args | preferred_username: preferred_username, summary: summary}
- end
- end
-
- defp prepare_args_for_report(args) do
- with {:reporter, %Actor{} = reporter_actor} <-
- {:reporter, Actors.get_actor!(args.reporter_id)},
- {:reported, %Actor{} = reported_actor} <-
- {:reported, Actors.get_actor!(args.reported_id)},
- content <- HTML.strip_tags(args.content),
- event <- Conversations.get_comment(Map.get(args, :event_id)),
- {:get_report_comments, comments} <-
- {:get_report_comments,
- Conversations.list_comments_by_actor_and_ids(
- reported_actor.id,
- Map.get(args, :comments_ids, [])
- )} do
- Map.merge(args, %{
- reporter: reporter_actor,
- reported: reported_actor,
- content: content,
- event: event,
- comments: comments
- })
- end
- end
-
- defp check_for_actor_key_rotation(%Actor{} = actor) do
- if Actors.should_rotate_actor_key(actor) do
- Actors.schedule_key_rotation(
- actor,
- Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
- )
- end
-
- :ok
- end
end
diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex
index aabaaf7d..04449cb3 100644
--- a/lib/federation/activity_pub/audience.ex
+++ b/lib/federation/activity_pub/audience.ex
@@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Share
alias Mobilizon.Storage.Repo
@@ -79,6 +79,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
def get_addressed_actors(mentioned_users, _), do: mentioned_users
+ def calculate_to_and_cc_from_mentions(
+ %Comment{discussion: %Discussion{actor_id: actor_id}} = _comment
+ ) do
+ with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
+ %{"to" => [members_url], "cc" => []}
+ end
+ end
+
def calculate_to_and_cc_from_mentions(%Comment{} = comment) do
with mentioned_actors <- Enum.map(comment.mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
@@ -96,6 +104,28 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
end
end
+ def calculate_to_and_cc_from_mentions(%Discussion{actor_id: actor_id}) do
+ with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
+ %{"to" => [members_url], "cc" => []}
+ end
+ end
+
+ def calculate_to_and_cc_from_mentions(%Event{
+ attributed_to: %Actor{members_url: members_url},
+ visibility: visibility
+ }) do
+ case visibility do
+ :public ->
+ %{"to" => [members_url, @ap_public], "cc" => []}
+
+ :unlisted ->
+ %{"to" => [members_url], "cc" => [@ap_public]}
+
+ :private ->
+ %{"to" => [members_url], "cc" => []}
+ end
+ end
+
def calculate_to_and_cc_from_mentions(%Event{} = event) do
with mentioned_actors <- Enum.map(event.mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex
new file mode 100644
index 00000000..d74cebda
--- /dev/null
+++ b/lib/federation/activity_pub/fetcher.ex
@@ -0,0 +1,74 @@
+defmodule Mobilizon.Federation.ActivityPub.Fetcher do
+ @moduledoc """
+ Module to handle direct URL ActivityPub fetches to remote content
+
+ If you need to first get cached data, see `Mobilizon.Federation.ActivityPub.fetch_object_from_url/2`
+ """
+ require Logger
+
+ alias Mobilizon.Federation.HTTPSignatures.Signature
+ alias Mobilizon.Federation.ActivityPub.{Relay, Transmogrifier}
+ alias Mobilizon.Service.HTTP.ActivityPub, as: ActivityPubClient
+
+ import Mobilizon.Federation.ActivityPub.Utils,
+ only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]
+
+ @spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
+ def fetch(url, options \\ []) do
+ on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
+
+ with date <- Signature.generate_date_header(),
+ headers <-
+ [{:Accept, "application/activity+json"}]
+ |> maybe_date_fetch(date)
+ |> sign_fetch(on_behalf_of, url, date),
+ client <-
+ ActivityPubClient.client(headers: headers),
+ {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
+ ActivityPubClient.get(client, url) do
+ {:ok, data}
+ end
+ end
+
+ @spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
+ def fetch_and_create(url, options \\ []) do
+ with {:ok, data} when is_map(data) <- fetch(url, options),
+ :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
+ :ok <- Logger.debug(inspect(data)),
+ {:origin_check, true} <- {:origin_check, origin_check?(url, data)},
+ params <- %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ "actor" => data["attributedTo"] || data["actor"],
+ "object" => data
+ } do
+ Transmogrifier.handle_incoming(params)
+ else
+ {:origin_check, false} ->
+ Logger.warn("Object origin check failed")
+ {:error, "Object origin check failed"}
+ end
+ end
+
+ @spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
+ def fetch_and_update(url, options \\ []) do
+ with {:ok, data} when is_map(data) <- fetch(url, options),
+ :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
+ :ok <- Logger.debug(inspect(data)),
+ {:origin_check, true} <- {:origin_check, origin_check?(url, data)},
+ params <- %{
+ "type" => "Update",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ "actor" => data["attributedTo"] || data["actor"],
+ "object" => data
+ } do
+ Transmogrifier.handle_incoming(params)
+ else
+ {:origin_check, false} ->
+ Logger.warn("Object origin check failed")
+ {:error, "Object origin check failed"}
+ end
+ end
+end
diff --git a/lib/federation/activity_pub/preloader.ex b/lib/federation/activity_pub/preloader.ex
new file mode 100644
index 00000000..b964c5b0
--- /dev/null
+++ b/lib/federation/activity_pub/preloader.ex
@@ -0,0 +1,30 @@
+defmodule Mobilizon.Federation.ActivityPub.Preloader do
+ @moduledoc """
+ Module to ensure entities are correctly preloaded
+ """
+
+ # TODO: Move me in a more appropriate place
+ alias Mobilizon.{Actors, Discussions, Events, Resources}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.{Comment, Discussion}
+ alias Mobilizon.Events.Event
+ alias Mobilizon.Resources.Resource
+ alias Mobilizon.Tombstone
+
+ def maybe_preload(%Event{url: url}),
+ do: {:ok, Events.get_public_event_by_url_with_preload!(url)}
+
+ def maybe_preload(%Comment{url: url}),
+ do: {:ok, Discussions.get_comment_from_url_with_preload!(url)}
+
+ def maybe_preload(%Discussion{} = discussion), do: {:ok, discussion}
+
+ def maybe_preload(%Resource{url: url}),
+ do: {:ok, Resources.get_resource_by_url_with_preloads(url)}
+
+ def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)}
+
+ def maybe_preload(%Tombstone{uri: _uri} = tombstone), do: {:ok, tombstone}
+
+ def maybe_preload(other), do: {:error, other}
+end
diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex
index ef7209e3..021742d9 100644
--- a/lib/federation/activity_pub/refresher.ex
+++ b/lib/federation/activity_pub/refresher.ex
@@ -3,24 +3,31 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Module that provides functions to explore and fetch collections on a group
"""
- alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
- alias Mobilizon.Federation.ActivityStream.Converter.Member, as: MemberConverter
- alias Mobilizon.Federation.ActivityStream.Converter.Resource, as: ResourceConverter
- alias Mobilizon.Federation.HTTPSignatures.Signature
- alias Mobilizon.Resources
+ alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier}
require Logger
- import Mobilizon.Federation.ActivityPub.Utils,
- only: [maybe_date_fetch: 2, sign_fetch: 4]
-
@spec fetch_group(String.t(), Actor.t()) :: :ok
def fetch_group(group_url, %Actor{} = on_behalf_of) do
- with {:ok, %Actor{resources_url: resources_url, members_url: members_url}} <-
+ with {:ok,
+ %Actor{
+ outbox_url: outbox_url,
+ resources_url: resources_url,
+ members_url: members_url,
+ posts_url: posts_url,
+ todos_url: todos_url,
+ discussions_url: discussions_url,
+ events_url: events_url
+ }} <-
ActivityPub.get_or_fetch_actor_by_url(group_url) do
+ fetch_collection(outbox_url, on_behalf_of)
fetch_collection(members_url, on_behalf_of)
fetch_collection(resources_url, on_behalf_of)
+ fetch_collection(posts_url, on_behalf_of)
+ fetch_collection(todos_url, on_behalf_of)
+ fetch_collection(discussions_url, on_behalf_of)
+ fetch_collection(events_url, on_behalf_of)
end
end
@@ -30,12 +37,28 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Logger.debug("Fetching and preparing collection from url")
Logger.debug(inspect(collection_url))
- with {:ok, data} <- fetch(collection_url, on_behalf_of) do
+ with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
Logger.debug("Fetch ok, passing to process_collection")
process_collection(data, on_behalf_of)
end
end
+ @spec fetch_element(String.t(), Actor.t()) :: any()
+ def fetch_element(url, %Actor{} = on_behalf_of) do
+ with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
+ case handling_element(data) do
+ {:ok, _activity, entity} ->
+ {:ok, entity}
+
+ {:ok, entity} ->
+ {:ok, entity}
+
+ err ->
+ {:error, err}
+ end
+ end
+ end
+
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
when type in ["OrderedCollection", "OrderedCollectionPage"] do
Logger.debug(
@@ -55,55 +78,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
when is_bitstring(first) do
Logger.debug("OrderedCollection has a first property pointing to an URI")
- with {:ok, data} <- fetch(first, on_behalf_of) do
+ with {:ok, data} <- Fetcher.fetch(first, on_behalf_of: on_behalf_of) do
Logger.debug("Fetched the collection for first property")
process_collection(data, on_behalf_of)
end
end
- defp handling_element(%{"type" => "Member"} = data) do
- Logger.debug("Handling Member element")
+ defp handling_element(data) when is_map(data) do
+ activity = %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ "actor" => data["actor"],
+ "attributedTo" => data["attributedTo"],
+ "object" => data
+ }
- data
- |> MemberConverter.as_to_model_data()
- |> Actors.create_member()
+ Transmogrifier.handle_incoming(activity)
end
- defp handling_element(%{"type" => type} = data)
- when type in ["Document", "ResourceCollection"] do
- Logger.debug("Handling Resource element")
-
- data
- |> ResourceConverter.as_to_model_data()
- |> Resources.create_resource()
- end
-
- defp fetch(url, %Actor{} = on_behalf_of) do
- with date <- Signature.generate_date_header(),
- headers <-
- [{:Accept, "application/activity+json"}]
- |> maybe_date_fetch(date)
- |> sign_fetch(on_behalf_of, url, date),
- %HTTPoison.Response{status_code: 200, body: body} <-
- HTTPoison.get!(url, headers,
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ),
- {:ok, data} <-
- Jason.decode(body) do
- {:ok, data}
- else
- # Actor is gone, probably deleted
- {:ok, %HTTPoison.Response{status_code: 410}} ->
- Logger.info("Response HTTP 410")
- {:error, :actor_deleted}
-
- {:origin_check, false} ->
- {:error, "Origin check failed"}
-
- e ->
- Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
- {:error, e}
- end
+ defp handling_element(uri) when is_binary(uri) do
+ ActivityPub.fetch_object_from_url(uri)
end
end
diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex
index 561a3203..29338dc0 100644
--- a/lib/federation/activity_pub/transmogrifier.ex
+++ b/lib/federation/activity_pub/transmogrifier.ex
@@ -8,17 +8,19 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
- alias Mobilizon.{Actors, Conversations, Events, Resources, Todos}
+ alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos}
alias Mobilizon.Actors.{Actor, Follower, Member}
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub
- alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
+ alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Utils}
+ alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
-
+ alias Mobilizon.Tombstone
alias Mobilizon.Web.Email.{Group, Participation}
require Logger
@@ -62,10 +64,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with object_data when is_map(object_data) <-
object |> Converter.Comment.as_to_model_data(),
{:existing_comment, {:error, :comment_not_found}} <-
- {:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
- {:ok, %Activity{} = activity, %Comment{} = comment} <-
- ActivityPub.create(:comment, object_data, false) do
- {:ok, activity, comment}
+ {:existing_comment, Discussions.get_comment_from_url_with_preload(object_data.url)},
+ object_data <- transform_object_data_for_discussion(object_data) do
+ # Check should be better
+
+ {:ok, %Activity{} = activity, entity} =
+ if is_data_for_comment_or_discussion?(object_data) do
+ Logger.debug("Chosing to create a regular comment")
+ ActivityPub.create(:comment, object_data, false)
+ else
+ Logger.debug("Chosing to initialize or add a comment to a conversation")
+ ActivityPub.create(:discussion, object_data, false)
+ end
+
+ {:ok, activity, entity}
else
{:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, comment}
@@ -100,6 +112,77 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
+ def handle_incoming(%{
+ "type" => "Create",
+ "object" => %{"type" => "Group", "id" => group_url} = _object
+ }) do
+ Logger.info("Handle incoming to create a group")
+
+ with {:ok, %Actor{} = group} <- ActivityPub.get_or_fetch_actor_by_url(group_url) do
+ {:ok, nil, group}
+ end
+ end
+
+ def handle_incoming(%{
+ "type" => "Create",
+ "object" => %{"type" => "Member"} = object
+ }) do
+ Logger.info("Handle incoming to create a member")
+
+ with object_data when is_map(object_data) <-
+ object |> Converter.Member.as_to_model_data(),
+ {:existing_member, nil} <-
+ {:existing_member, Actors.get_member_by_url(object_data.url)},
+ {:ok, %Activity{} = activity, %Member{} = member} <-
+ ActivityPub.join_group(object_data, false) do
+ {:ok, activity, member}
+ else
+ {:existing_member, %Member{} = member} ->
+ {:ok, nil, member}
+ end
+ end
+
+ def handle_incoming(%{
+ "type" => "Create",
+ "object" =>
+ %{"type" => "Article", "actor" => _actor, "attributedTo" => _attributed_to} = object
+ }) do
+ Logger.info("Handle incoming to create articles")
+
+ with object_data when is_map(object_data) <-
+ object |> Converter.Post.as_to_model_data(),
+ {:existing_post, nil} <-
+ {:existing_post, Posts.get_post_by_url(object_data.url)},
+ {:ok, %Activity{} = activity, %Post{} = post} <-
+ ActivityPub.create(:post, object_data, false) do
+ {:ok, activity, post}
+ else
+ {:existing_post, %Post{} = post} ->
+ {:ok, nil, post}
+ end
+ end
+
+ # This is a hack to handle Tombstones fetched by AP
+ def handle_incoming(%{
+ "type" => "Create",
+ "object" => %{"type" => "Tombstone", "id" => object_url} = _object
+ }) do
+ Logger.info("Handle incoming to create a tombstone")
+
+ case ActivityPub.fetch_object_from_url(object_url, force: true) do
+ # We already have the tombstone, object is probably already deleted
+ {:ok, %Tombstone{} = tombstone} ->
+ {:ok, nil, tombstone}
+
+ # Hack because deleted comments
+ {:ok, %Comment{deleted_at: deleted_at} = comment} when not is_nil(deleted_at) ->
+ {:ok, nil, comment}
+
+ {:ok, entity} ->
+ ActivityPub.delete(entity, Relay.get_actor(), false)
+ end
+ end
+
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
@@ -165,7 +248,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Logger.info("Handle incoming to create a resource")
Logger.debug(inspect(data))
- group_url = hd(to)
+ group_url = if is_list(to) and not is_nil(to), do: hd(to), else: to
with {:existing_resource, nil} <-
{:existing_resource, Resources.get_resource_by_url(object_url)},
@@ -175,8 +258,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
{:ok, %Activity{} = activity, %Resource{} = resource} <-
ActivityPub.create(:resource, object_data, false),
- {:ok, %Actor{type: :Group, id: group_id} = group} <-
- ActivityPub.get_or_fetch_actor_by_url(group_url),
+ %Actor{type: :Group, id: group_id} = group <-
+ Actors.get_group_by_members_url(group_url),
announce_id <- "#{object_url}/announces/#{group_id}",
{:ok, _activity, _resource} <- ActivityPub.announce(group, object, announce_id) do
{:ok, activity, resource}
@@ -190,7 +273,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:error
{:error, e} ->
- Logger.error(inspect(e))
+ Logger.debug(inspect(e))
:error
end
end
@@ -261,23 +344,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data
) do
- with actor <- Utils.get_actor(data),
- # TODO: Is the following line useful?
- {:ok, %Actor{id: actor_id, suspended: false} = _actor} <-
- ActivityPub.get_or_fetch_actor_by_url(actor),
+ with actor_url <- Utils.get_actor(data),
+ {:ok, %Actor{id: actor_id, suspended: false} = actor} <-
+ ActivityPub.get_or_fetch_actor_by_url(actor_url),
:ok <- Logger.debug("Fetching contained object"),
- {:ok, object} <- fetch_obj_helper_as_activity_streams(object),
- :ok <- Logger.debug("Handling contained object"),
- create_data <- Utils.make_create_data(object),
- :ok <- Logger.debug(inspect(object)),
- {:ok, _activity, entity} <- handle_incoming(create_data),
- :ok <- Logger.debug("Finished processing contained object"),
- {:ok, activity} <- ActivityPub.create_activity(data, false),
- {:ok, %Actor{id: object_owner_actor_id}} <-
- ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
- {:ok, %Mobilizon.Share{} = _share} <-
- Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
- {:ok, activity, entity}
+ {:ok, entity} <-
+ object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor),
+ :ok <- eventually_create_share(object, entity, actor_id) do
+ {:ok, nil, entity}
else
e ->
Logger.debug(inspect(e))
@@ -296,7 +370,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <-
object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
- ActivityPub.update(:actor, old_actor, object_data, false) do
+ ActivityPub.update(old_actor, object_data, false) do
{:ok, activity, new_actor}
else
e ->
@@ -317,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- Converter.Event.as_to_model_data(object),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
{:ok, %Activity{} = activity, %Event{} = new_event} <-
- ActivityPub.update(:event, old_event, object_data, false) do
+ ActivityPub.update(old_event, object_data, false) do
{:ok, activity, new_event}
else
_e ->
@@ -325,6 +399,42 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
+ def handle_incoming(
+ %{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} =
+ update_data
+ ) do
+ with actor <- Utils.get_actor(update_data),
+ {:ok, %Actor{url: actor_url, suspended: false}} <-
+ ActivityPub.get_or_fetch_actor_by_url(actor),
+ {:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
+ object_data <- Converter.Comment.as_to_model_data(object),
+ {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
+ object_data <- transform_object_data_for_discussion(object_data),
+ {:ok, %Activity{} = activity, new_entity} <-
+ ActivityPub.update(old_entity, object_data, false) do
+ {:ok, activity, new_entity}
+ else
+ _e ->
+ :error
+ end
+ end
+
+ def handle_incoming(%{
+ "type" => "Update",
+ "object" => %{"type" => "Tombstone"} = object,
+ "actor" => _actor
+ }) do
+ Logger.info("Handle incoming to update a tombstone")
+
+ with object_url <- Utils.get_url(object),
+ {:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do
+ ActivityPub.delete(entity, Relay.get_actor(), false)
+ else
+ {:ok, %Tombstone{} = tombstone} ->
+ {:ok, nil, tombstone}
+ end
+ end
+
def handle_incoming(
%{
"type" => "Undo",
@@ -367,21 +477,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
- # TODO: We presently assume that any actor on the same origin domain as the object being
- # deleted has the rights to delete that object. A better way to validate whether or not
- # the object should be deleted is to refetch the object URI, which should return either
- # an error or a tombstone. This would allow us to verify that a deletion actually took
- # place.
+ # We assume everyone on the same instance as the object
+ # or who is member of a group has the right to delete the object
def handle_incoming(
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
) do
- with actor <- Utils.get_actor(data),
- {:ok, %Actor{url: actor_url}} <- ActivityPub.get_or_fetch_actor_by_url(actor),
+ with actor_url <- Utils.get_actor(data),
+ {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
object_id <- Utils.get_url(object),
- {:origin_check, true} <-
- {:origin_check, Utils.origin_check_from_id?(actor_url, object_id)},
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
- {:ok, activity, object} <- ActivityPub.delete(object, false) do
+ {:origin_check, true} <-
+ {:origin_check,
+ Utils.origin_check_from_id?(actor_url, object_id) ||
+ Utils.activity_actor_is_group_member?(actor, object)},
+ {:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
{:ok, activity, object}
else
{:origin_check, false} ->
@@ -449,6 +558,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
"target" => target
} = data
) do
+ Logger.info("Handle incoming to invite someone")
+
with {:ok, %Actor{} = actor} <-
data |> Utils.get_actor() |> ActivityPub.get_or_fetch_actor_by_url(),
{:ok, object} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
@@ -485,7 +596,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# end
def handle_incoming(object) do
- Logger.info("Handing something not supported")
+ Logger.info("Handing something with type #{object["type"]} not supported")
Logger.debug(inspect(object))
{:error, :not_supported}
end
@@ -657,6 +768,52 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
+ # If the object has been announced by a group let's use one of our members to fetch it
+ @spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) ::
+ {:ok, struct()} | {:error, any()}
+ defp fetch_object_optionnally_authenticated(url, %Actor{type: :Group, id: group_id}) do
+ case Actors.get_single_group_member_actor(group_id) do
+ %Actor{} = actor ->
+ ActivityPub.fetch_object_from_url(url, on_behalf_of: actor, force: true)
+
+ _err ->
+ fetch_object_optionnally_authenticated(url, nil)
+ end
+ end
+
+ defp fetch_object_optionnally_authenticated(url, _),
+ do: ActivityPub.fetch_object_from_url(url, force: true)
+
+ defp eventually_create_share(object, entity, actor_id) do
+ with object_id <- object |> Utils.get_url(),
+ %Actor{id: object_owner_actor_id} <- Ownable.actor(entity) do
+ {:ok, %Mobilizon.Share{} = _share} =
+ Mobilizon.Share.create(object_id, actor_id, object_owner_actor_id)
+ end
+
+ :ok
+ end
+
+ @spec is_data_for_comment_or_discussion?(map()) :: boolean()
+ defp is_data_for_comment_or_discussion?(object_data) do
+ (not Map.has_key?(object_data, :title) or
+ is_nil(object_data.title) or object_data.title == "") and
+ is_nil(object_data.discussion_id)
+ end
+
+ # Comment and conversations have different attributes for actor and groups
+ defp transform_object_data_for_discussion(object_data) do
+ # Basic comment
+ if is_data_for_comment_or_discussion?(object_data) do
+ object_data
+ else
+ # Conversation
+ object_data
+ |> Map.put(:creator_id, object_data.actor_id)
+ |> Map.put(:actor_id, object_data.attributed_to_id)
+ end
+ end
+
defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <-
diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex
new file mode 100644
index 00000000..c9360959
--- /dev/null
+++ b/lib/federation/activity_pub/types/actors.ex
@@ -0,0 +1,74 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
+ @moduledoc false
+ alias Mobilizon.Actors
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub.Audience
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.GraphQL.API.Utils, as: APIUtils
+ alias Mobilizon.Service.Formatter.HTML
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with args <- prepare_args_for_actor(args),
+ {:ok, %Actor{} = actor} <- Actors.create_actor(args),
+ actor_as_data <- Convertible.model_to_as(actor),
+ audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
+ create_data <-
+ make_create_data(actor_as_data, Map.merge(audience, additional)) do
+ {:ok, actor, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
+ def update(%Actor{} = old_actor, args, additional) do
+ with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
+ actor_as_data <- Convertible.model_to_as(new_actor),
+ {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(new_actor),
+ additional <- Map.merge(additional, %{"actor" => old_actor.url}),
+ update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
+ {:ok, new_actor, update_data}
+ end
+ end
+
+ @impl Entity
+ def delete(
+ %Actor{followers_url: followers_url, url: target_actor_url} = target_actor,
+ %Actor{url: actor_url} = actor,
+ local
+ ) do
+ activity_data = %{
+ "type" => "Delete",
+ "actor" => actor_url,
+ "object" => Convertible.model_to_as(target_actor),
+ "id" => target_actor_url <> "/delete",
+ "to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"]
+ }
+
+ # We completely delete the actor if activity is remote
+ with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do
+ {:ok, activity_data, actor, target_actor}
+ end
+ end
+
+ def actor(%Actor{} = actor), do: actor
+
+ def group_actor(%Actor{} = _actor), do: nil
+
+ defp prepare_args_for_actor(args) do
+ with preferred_username <-
+ args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
+ summary <- args |> Map.get(:summary, "") |> String.trim(),
+ {summary, _mentions, _tags} <-
+ summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
+ %{args | preferred_username: preferred_username, summary: summary}
+ end
+ end
+end
diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex
new file mode 100644
index 00000000..5c1ce153
--- /dev/null
+++ b/lib/federation/activity_pub/types/comments.ex
@@ -0,0 +1,149 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
+ @moduledoc false
+ alias Mobilizon.{Actors, Discussions, Events}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.{Comment, Discussion}
+ alias Mobilizon.Events.Event
+ alias Mobilizon.Federation.ActivityPub.Audience
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.GraphQL.API.Utils, as: APIUtils
+ alias Mobilizon.Share
+ alias Mobilizon.Tombstone
+ alias Mobilizon.Web.Endpoint
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+ require Logger
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with args <- prepare_args_for_comment(args),
+ {:ok, %Comment{discussion_id: discussion_id} = comment} <-
+ Discussions.create_comment(args),
+ :ok <- maybe_publish_graphql_subscription(discussion_id),
+ comment_as_data <- Convertible.model_to_as(comment),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(comment),
+ create_data <-
+ make_create_data(comment_as_data, Map.merge(audience, additional)) do
+ {:ok, comment, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
+ def update(%Comment{} = old_comment, args, additional) do
+ with args <- prepare_args_for_comment(args),
+ {:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
+ {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
+ comment_as_data <- Convertible.model_to_as(new_comment),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(new_comment),
+ update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
+ {:ok, new_comment, update_data}
+ else
+ err ->
+ Logger.error("Something went wrong while creating an update activity")
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ @spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()}
+ def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do
+ activity_data = %{
+ "type" => "Delete",
+ "actor" => actor.url,
+ "object" => Convertible.model_to_as(comment),
+ "id" => url <> "/delete",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"]
+ }
+
+ with audience <-
+ Audience.calculate_to_and_cc_from_mentions(comment),
+ {:ok, %Comment{} = comment} <- Discussions.delete_comment(comment),
+ # Preload to be sure
+ %Comment{} = comment <- Discussions.get_comment_with_preload(comment.id),
+ {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
+ {:ok, %Tombstone{} = _tombstone} <-
+ Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
+ Share.delete_all_by_uri(comment.url)
+ {:ok, Map.merge(activity_data, audience), actor, comment}
+ end
+ end
+
+ def actor(%Comment{actor: %Actor{} = actor}), do: actor
+
+ def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
+ do: Actors.get_actor(actor_id)
+
+ def actor(_), do: nil
+
+ def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group
+
+ def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
+ do: Actors.get_actor(attributed_to_id)
+
+ def group_actor(_), do: nil
+
+ # Prepare and sanitize arguments for comments
+ defp prepare_args_for_comment(args) do
+ with in_reply_to_comment <-
+ args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
+ event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
+ args <- Map.update(args, :visibility, :public, & &1),
+ {text, mentions, tags} <-
+ APIUtils.make_content_html(
+ args |> Map.get(:text, "") |> String.trim(),
+ # Can't put additional tags on a comment
+ [],
+ "text/html"
+ ),
+ tags <- ConverterUtils.fetch_tags(tags),
+ mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
+ args <-
+ Map.merge(args, %{
+ actor_id: Map.get(args, :actor_id),
+ text: text,
+ mentions: mentions,
+ tags: tags,
+ event: event,
+ in_reply_to_comment: in_reply_to_comment,
+ in_reply_to_comment_id:
+ if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
+ origin_comment_id:
+ if(is_nil(in_reply_to_comment),
+ do: nil,
+ else: Comment.get_thread_id(in_reply_to_comment)
+ )
+ }) do
+ args
+ end
+ end
+
+ @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
+ defp handle_event_for_comment(event_id) when not is_nil(event_id) do
+ case Events.get_event_with_preload(event_id) do
+ {:ok, %Event{} = event} -> event
+ {:error, :event_not_found} -> nil
+ end
+ end
+
+ defp handle_event_for_comment(nil), do: nil
+
+ defp maybe_publish_graphql_subscription(nil), do: :ok
+
+ defp maybe_publish_graphql_subscription(discussion_id) do
+ with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do
+ Absinthe.Subscription.publish(Endpoint, discussion,
+ discussion_comment_changed: discussion.slug
+ )
+
+ :ok
+ end
+ end
+end
diff --git a/lib/federation/activity_pub/types/discussions.ex b/lib/federation/activity_pub/types/discussions.ex
new file mode 100644
index 00000000..a27d3c2c
--- /dev/null
+++ b/lib/federation/activity_pub/types/discussions.ex
@@ -0,0 +1,115 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
+ @moduledoc false
+
+ alias Mobilizon.{Actors, Discussions}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.{Comment, Discussion}
+ alias Mobilizon.Federation.ActivityPub.Audience
+ alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity}
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Storage.Repo
+ alias Mobilizon.Web.Endpoint
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+ require Logger
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
+ with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
+ {:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <-
+ Discussions.reply_to_discussion(discussion, args),
+ %Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id),
+ :ok <- maybe_publish_graphql_subscription(discussion),
+ comment_as_data <- Convertible.model_to_as(last_comment),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(discussion),
+ create_data <-
+ make_create_data(comment_as_data, Map.merge(audience, additional)) do
+ {:ok, discussion, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with {:ok, %Discussion{} = discussion} <-
+ Discussions.create_discussion(args),
+ discussion_as_data <- Convertible.model_to_as(discussion),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(discussion),
+ create_data <-
+ make_create_data(discussion_as_data, Map.merge(audience, additional)) do
+ {:ok, discussion, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any()
+ def update(%Discussion{} = old_discussion, args, additional) do
+ with {:ok, %Discussion{} = new_discussion} <-
+ Discussions.update_discussion(old_discussion, args),
+ {:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
+ discussion_as_data <- Convertible.model_to_as(new_discussion),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(new_discussion),
+ update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
+ {:ok, new_discussion, update_data}
+ else
+ err ->
+ Logger.error("Something went wrong while creating an update activity")
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ @spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()}
+ def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do
+ stream =
+ discussion.comments
+ |> Enum.map(
+ &Repo.preload(&1, [
+ :actor,
+ :attributed_to,
+ :in_reply_to_comment,
+ :mentions,
+ :origin_comment,
+ :discussion,
+ :tags,
+ :replies
+ ])
+ )
+ |> Enum.map(&Map.put(&1, :event, nil))
+ |> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end)
+
+ Stream.run(stream)
+
+ with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do
+ # This is just fake
+ activity_data = %{
+ "type" => "Delete",
+ "actor" => actor.url,
+ "object" => Convertible.model_to_as(discussion),
+ "id" => url <> "/delete",
+ "to" => [group.members_url]
+ }
+
+ {:ok, activity_data, actor, discussion}
+ end
+ end
+
+ def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id)
+
+ def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
+
+ @spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
+ defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do
+ Absinthe.Subscription.publish(Endpoint, discussion,
+ discussion_comment_changed: discussion.slug
+ )
+
+ :ok
+ end
+end
diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex
new file mode 100644
index 00000000..f2756551
--- /dev/null
+++ b/lib/federation/activity_pub/types/entity.ex
@@ -0,0 +1,151 @@
+alias Mobilizon.Federation.ActivityPub.Types.{
+ Actors,
+ Comments,
+ Discussions,
+ Entity,
+ Events,
+ Managable,
+ Ownable,
+ Posts,
+ Resources,
+ Todos,
+ TodoLists,
+ Tombstones
+}
+
+alias Mobilizon.Actors.Actor
+alias Mobilizon.Events.Event
+alias Mobilizon.Discussions.{Comment, Discussion}
+alias Mobilizon.Posts.Post
+alias Mobilizon.Resources.Resource
+alias Mobilizon.Todos.{Todo, TodoList}
+alias Mobilizon.Federation.ActivityStream
+alias Mobilizon.Tombstone
+
+defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
+ @moduledoc """
+ ActivityPub entity behaviour
+ """
+ @type t :: %{id: String.t()}
+
+ @callback create(data :: any(), additionnal :: map()) ::
+ {:ok, t(), ActivityStream.t()}
+
+ @callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
+ {:ok, t(), ActivityStream.t()}
+
+ @callback delete(struct :: t(), Actor.t(), local :: boolean()) ::
+ {:ok, ActivityStream.t(), Actor.t(), t()}
+end
+
+defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
+ @moduledoc """
+ ActivityPub entity Managable protocol.
+ """
+
+ @spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
+ @doc """
+ Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
+ """
+ def update(entity, attrs, additionnal)
+
+ @spec delete(Entity.t(), Actor.t(), boolean()) ::
+ {:ok, ActivityStream.t(), Actor.t(), Entity.t()}
+ @doc "Deletes an entity and returns the activitystream representation for it"
+ def delete(entity, actor, local)
+end
+
+defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
+ @spec group_actor(Entity.t()) :: Actor.t() | nil
+ @doc "Returns an eventual group for the entity"
+ def group_actor(entity)
+
+ @spec actor(Entity.t()) :: Actor.t() | nil
+ @doc "Returns the actor for the entity"
+ def actor(entity)
+end
+
+defimpl Managable, for: Event do
+ defdelegate update(entity, attrs, additionnal), to: Events
+ defdelegate delete(entity, actor, local), to: Events
+end
+
+defimpl Ownable, for: Event do
+ defdelegate group_actor(entity), to: Events
+ defdelegate actor(entity), to: Events
+end
+
+defimpl Managable, for: Comment do
+ defdelegate update(entity, attrs, additionnal), to: Comments
+ defdelegate delete(entity, actor, local), to: Comments
+end
+
+defimpl Ownable, for: Comment do
+ defdelegate group_actor(entity), to: Comments
+ defdelegate actor(entity), to: Comments
+end
+
+defimpl Managable, for: Post do
+ defdelegate update(entity, attrs, additionnal), to: Posts
+ defdelegate delete(entity, actor, local), to: Posts
+end
+
+defimpl Ownable, for: Post do
+ defdelegate group_actor(entity), to: Posts
+ defdelegate actor(entity), to: Posts
+end
+
+defimpl Managable, for: Actor do
+ defdelegate update(entity, attrs, additionnal), to: Actors
+ defdelegate delete(entity, actor, local), to: Actors
+end
+
+defimpl Ownable, for: Actor do
+ defdelegate group_actor(entity), to: Actors
+ defdelegate actor(entity), to: Actors
+end
+
+defimpl Managable, for: TodoList do
+ defdelegate update(entity, attrs, additionnal), to: TodoLists
+ defdelegate delete(entity, actor, local), to: TodoLists
+end
+
+defimpl Ownable, for: TodoList do
+ defdelegate group_actor(entity), to: TodoLists
+ defdelegate actor(entity), to: TodoLists
+end
+
+defimpl Managable, for: Todo do
+ defdelegate update(entity, attrs, additionnal), to: Todos
+ defdelegate delete(entity, actor, local), to: Todos
+end
+
+defimpl Ownable, for: Todo do
+ defdelegate group_actor(entity), to: Todos
+ defdelegate actor(entity), to: Todos
+end
+
+defimpl Managable, for: Resource do
+ defdelegate update(entity, attrs, additionnal), to: Resources
+ defdelegate delete(entity, actor, local), to: Resources
+end
+
+defimpl Ownable, for: Resource do
+ defdelegate group_actor(entity), to: Resources
+ defdelegate actor(entity), to: Resources
+end
+
+defimpl Managable, for: Discussion do
+ defdelegate update(entity, attrs, additionnal), to: Discussions
+ defdelegate delete(entity, actor, local), to: Discussions
+end
+
+defimpl Ownable, for: Discussion do
+ defdelegate group_actor(entity), to: Discussions
+ defdelegate actor(entity), to: Discussions
+end
+
+defimpl Ownable, for: Tombstone do
+ defdelegate group_actor(entity), to: Tombstones
+ defdelegate actor(entity), to: Tombstones
+end
diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex
new file mode 100644
index 00000000..ea908266
--- /dev/null
+++ b/lib/federation/activity_pub/types/events.ex
@@ -0,0 +1,203 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Events do
+ @moduledoc false
+ alias Mobilizon.Actors
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Events, as: EventsManager
+ alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.Audience
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.GraphQL.API.Utils, as: APIUtils
+ alias Mobilizon.Service.Formatter.HTML
+ alias Mobilizon.Service.Notifications.Scheduler
+ alias Mobilizon.Share
+ alias Mobilizon.Tombstone
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+ require Logger
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with args <- prepare_args_for_event(args),
+ {:ok, %Event{} = event} <- EventsManager.create_event(args),
+ event_as_data <- Convertible.model_to_as(event),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(event),
+ create_data <-
+ make_create_data(event_as_data, Map.merge(audience, additional)) do
+ {:ok, event, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
+ def update(%Event{} = old_event, args, additional) do
+ with args <- prepare_args_for_event(args),
+ {:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
+ {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
+ event_as_data <- Convertible.model_to_as(new_event),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(new_event),
+ update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
+ {:ok, new_event, update_data}
+ else
+ err ->
+ Logger.error("Something went wrong while creating an update activity")
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ @spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()}
+ def delete(%Event{url: url} = event, %Actor{} = actor, _local) do
+ activity_data = %{
+ "type" => "Delete",
+ "actor" => actor.url,
+ "object" => Convertible.model_to_as(event),
+ "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
+ "id" => url <> "/delete"
+ }
+
+ with audience <-
+ Audience.calculate_to_and_cc_from_mentions(event),
+ {:ok, %Event{} = event} <- EventsManager.delete_event(event),
+ {:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
+ {:ok, %Tombstone{} = _tombstone} <-
+ Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
+ Share.delete_all_by_uri(event.url)
+ {:ok, Map.merge(activity_data, audience), actor, event}
+ end
+ end
+
+ def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
+
+ def actor(%Event{organizer_actor_id: organizer_actor_id}),
+ do: Actors.get_actor(organizer_actor_id)
+
+ def actor(_), do: nil
+
+ def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
+
+ def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
+ do: Actors.get_actor(attributed_to_id)
+
+ def group_actor(_), do: nil
+
+ def join(%Event{} = event, %Actor{} = actor, _local, additional) do
+ with {:maximum_attendee_capacity, true} <-
+ {:maximum_attendee_capacity, check_attendee_capacity(event)},
+ role <-
+ additional
+ |> Map.get(:metadata, %{})
+ |> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
+ {:ok, %Participant{} = participant} <-
+ Mobilizon.Events.create_participant(%{
+ role: role,
+ event_id: event.id,
+ actor_id: actor.id,
+ url: Map.get(additional, :url),
+ metadata:
+ additional
+ |> Map.get(:metadata, %{})
+ |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
+ }),
+ join_data <- Convertible.model_to_as(participant),
+ audience <-
+ Audience.calculate_to_and_cc_from_mentions(participant) do
+ approve_if_default_role_is_participant(
+ event,
+ Map.merge(join_data, audience),
+ participant,
+ role
+ )
+ else
+ {:maximum_attendee_capacity, err} ->
+ {:maximum_attendee_capacity, err}
+ end
+ end
+
+ defp check_attendee_capacity(%Event{options: options} = event) do
+ with maximum_attendee_capacity <-
+ Map.get(options, :maximum_attendee_capacity) || 0 do
+ maximum_attendee_capacity == 0 ||
+ Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
+ end
+ end
+
+ # Set the participant to approved if the default role for new participants is :participant
+ defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
+ if event.local do
+ cond do
+ Mobilizon.Events.get_default_participant_role(event) === :participant &&
+ role == :participant ->
+ {:accept,
+ ActivityPub.accept(
+ :join,
+ participant,
+ true,
+ %{"actor" => event.organizer_actor.url}
+ )}
+
+ Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
+ role == :not_approved ->
+ Scheduler.pending_participation_notification(event)
+ {:ok, activity_data, participant}
+
+ true ->
+ {:ok, activity_data, participant}
+ end
+ else
+ {:ok, activity_data, participant}
+ end
+ end
+
+ # Prepare and sanitize arguments for events
+ defp prepare_args_for_event(args) do
+ # If title is not set: we are not updating it
+ args =
+ if Map.has_key?(args, :title) && !is_nil(args.title),
+ do: Map.update(args, :title, "", &String.trim/1),
+ else: args
+
+ # If we've been given a description (we might not get one if updating)
+ # sanitize it, HTML it, and extract tags & mentions from it
+ args =
+ if Map.has_key?(args, :description) && !is_nil(args.description) do
+ {description, mentions, tags} =
+ APIUtils.make_content_html(
+ String.trim(args.description),
+ Map.get(args, :tags, []),
+ "text/html"
+ )
+
+ mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
+
+ Map.merge(args, %{
+ description: description,
+ mentions: mentions,
+ tags: tags
+ })
+ else
+ args
+ end
+
+ # Check that we can only allow anonymous participation if our instance allows it
+ {_, options} =
+ Map.get_and_update(
+ Map.get(args, :options, %{anonymous_participation: false}),
+ :anonymous_participation,
+ fn value ->
+ {value, value && Mobilizon.Config.anonymous_participation?()}
+ end
+ )
+
+ args = Map.put(args, :options, options)
+
+ Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
+ end
+end
diff --git a/lib/federation/activity_pub/types/posts.ex b/lib/federation/activity_pub/types/posts.ex
new file mode 100644
index 00000000..51f3aadf
--- /dev/null
+++ b/lib/federation/activity_pub/types/posts.ex
@@ -0,0 +1,93 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
+ @moduledoc false
+ alias Mobilizon.{Actors, Posts}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Posts.Post
+ require Logger
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+
+ @behaviour Entity
+
+ @impl Entity
+ def create(args, additional) do
+ with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
+ {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
+ Posts.create_post(args),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
+ post_as_data <-
+ Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
+ audience <- %{
+ "to" => [group.members_url],
+ "cc" => [],
+ "actor" => creator_url,
+ "attributedTo" => [creator_url]
+ } do
+ create_data = make_create_data(post_as_data, Map.merge(audience, additional))
+
+ {:ok, post, create_data}
+ else
+ err ->
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ def update(%Post{} = post, args, additional) do
+ with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
+ {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
+ Posts.update_post(post, args),
+ {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
+ post_as_data <-
+ Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
+ audience <- %{
+ "to" => [group.members_url],
+ "cc" => [],
+ "actor" => creator_url,
+ "attributedTo" => [creator_url]
+ } do
+ update_data = make_update_data(post_as_data, Map.merge(audience, additional))
+
+ {:ok, post, update_data}
+ else
+ err ->
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ def delete(
+ %Post{
+ url: url,
+ attributed_to: %Actor{url: group_url}
+ } = post,
+ %Actor{url: actor_url} = actor,
+ _local
+ ) do
+ activity_data = %{
+ "actor" => actor_url,
+ "type" => "Delete",
+ "object" => Convertible.model_to_as(post),
+ "id" => url <> "/delete",
+ "to" => [group_url]
+ }
+
+ with {:ok, _post} <- Posts.delete_post(post),
+ {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do
+ {:ok, activity_data, actor, post}
+ end
+ end
+
+ def actor(%Post{author_id: author_id}),
+ do: Actors.get_actor(author_id)
+
+ def group_actor(%Post{attributed_to_id: attributed_to_id}),
+ do: Actors.get_actor(attributed_to_id)
+end
diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex
new file mode 100644
index 00000000..ded8a188
--- /dev/null
+++ b/lib/federation/activity_pub/types/reports.ex
@@ -0,0 +1,43 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
+ @moduledoc false
+ alias Mobilizon.{Actors, Discussions, Reports}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Reports.Report
+ alias Mobilizon.Service.Formatter.HTML
+ require Logger
+
+ def flag(args, local \\ false, _additional \\ %{}) do
+ with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
+ {:create_report, {:ok, %Report{} = report}} <-
+ {:create_report, Reports.create_report(args)},
+ report_as_data <- Convertible.model_to_as(report),
+ cc <- if(local, do: [report.reported.url], else: []),
+ report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
+ {report, report_as_data}
+ end
+ end
+
+ defp prepare_args_for_report(args) do
+ with {:reporter, %Actor{} = reporter_actor} <-
+ {:reporter, Actors.get_actor!(args.reporter_id)},
+ {:reported, %Actor{} = reported_actor} <-
+ {:reported, Actors.get_actor!(args.reported_id)},
+ content <- HTML.strip_tags(args.content),
+ event <- Discussions.get_comment(Map.get(args, :event_id)),
+ {:get_report_comments, comments} <-
+ {:get_report_comments,
+ Discussions.list_comments_by_actor_and_ids(
+ reported_actor.id,
+ Map.get(args, :comments_ids, [])
+ )} do
+ Map.merge(args, %{
+ reporter: reporter_actor,
+ reported: reported_actor,
+ content: content,
+ event: event,
+ comments: comments
+ })
+ end
+ end
+end
diff --git a/lib/federation/activity_pub/types/resources.ex b/lib/federation/activity_pub/types/resources.ex
new file mode 100644
index 00000000..06a21309
--- /dev/null
+++ b/lib/federation/activity_pub/types/resources.ex
@@ -0,0 +1,157 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
+ @moduledoc false
+ alias Mobilizon.{Actors, Resources}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Resources.Resource
+ alias Mobilizon.Service.RichMedia.Parser
+ require Logger
+
+ import Mobilizon.Federation.ActivityPub.Utils,
+ only: [make_create_data: 2, make_update_data: 2, make_add_data: 3, make_move_data: 4]
+
+ @behaviour Entity
+
+ @impl Entity
+ def create(%{type: type} = args, additional) do
+ args =
+ case type do
+ :folder ->
+ args
+
+ _ ->
+ case Parser.parse(Map.get(args, :resource_url)) do
+ {:ok, metadata} ->
+ Map.put(args, :metadata, metadata)
+
+ _ ->
+ args
+ end
+ end
+
+ with {:ok,
+ %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
+ Resources.create_resource(args),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
+ resource_as_data <-
+ Convertible.model_to_as(%{resource | actor: group, creator: creator}),
+ audience <- %{
+ "to" => [group.members_url],
+ "cc" => [],
+ "actor" => creator_url,
+ "attributedTo" => [creator_url]
+ } do
+ create_data =
+ case parent_id do
+ nil ->
+ make_create_data(resource_as_data, Map.merge(audience, additional))
+
+ parent_id ->
+ # In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
+ parent = Resources.get_resource(parent_id)
+ make_add_data(resource_as_data, parent, Map.merge(audience, additional))
+ end
+
+ {:ok, resource, create_data}
+ else
+ err ->
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ def update(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
+ move(old_resource, args, additional)
+ end
+
+ # Simple rename
+ def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
+ with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
+ Resources.update_resource(old_resource, %{title: title}),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ %Actor{url: creator_url} <- Actors.get_actor(creator_id),
+ resource_as_data <-
+ Convertible.model_to_as(%{resource | actor: group}),
+ audience <- %{
+ "to" => [group.members_url],
+ "cc" => [],
+ "actor" => creator_url,
+ "attributedTo" => [creator_url]
+ },
+ update_data <-
+ make_update_data(resource_as_data, Map.merge(audience, additional)) do
+ {:ok, resource, update_data}
+ else
+ err ->
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ def move(
+ %Resource{parent_id: old_parent_id} = old_resource,
+ %{parent_id: _new_parent_id} = args,
+ additional
+ ) do
+ with {:ok,
+ %Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
+ resource} <-
+ Resources.update_resource(old_resource, args),
+ old_parent <- Resources.get_resource(old_parent_id),
+ new_parent <- Resources.get_resource(new_parent_id),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ %Actor{url: creator_url} <- Actors.get_actor(creator_id),
+ resource_as_data <-
+ Convertible.model_to_as(%{resource | actor: group}),
+ audience <- %{
+ "to" => [group.members_url],
+ "cc" => [],
+ "actor" => creator_url,
+ "attributedTo" => [creator_url]
+ },
+ move_data <-
+ make_move_data(
+ resource_as_data,
+ old_parent,
+ new_parent,
+ Map.merge(audience, additional)
+ ) do
+ {:ok, resource, move_data}
+ else
+ err ->
+ Logger.debug(inspect(err))
+ err
+ end
+ end
+
+ @impl Entity
+ def delete(
+ %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
+ %Actor{url: actor_url} = actor,
+ _local
+ ) do
+ Logger.debug("Building Delete Resource activity")
+
+ activity_data = %{
+ "actor" => actor_url,
+ "attributedTo" => [group_url],
+ "type" => "Delete",
+ "object" => Convertible.model_to_as(resource),
+ "id" => url <> "/delete",
+ "to" => [members_url]
+ }
+
+ with {:ok, _resource} <- Resources.delete_resource(resource),
+ {:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do
+ {:ok, activity_data, actor, resource}
+ end
+ end
+
+ def actor(%Resource{creator_id: creator_id}),
+ do: Actors.get_actor(creator_id)
+
+ def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
+end
diff --git a/lib/federation/activity_pub/types/todo_lists.ex b/lib/federation/activity_pub/types/todo_lists.ex
new file mode 100644
index 00000000..dedf9e48
--- /dev/null
+++ b/lib/federation/activity_pub/types/todo_lists.ex
@@ -0,0 +1,69 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
+ @moduledoc false
+ alias Mobilizon.{Actors, Todos}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Todos.TodoList
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+ require Logger
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
+ audience <- %{"to" => [group.members_url], "cc" => []},
+ create_data <-
+ make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
+ {:ok, todo_list, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any
+ def update(%TodoList{} = old_todo_list, args, additional) do
+ with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
+ Todos.update_todo_list(old_todo_list, args),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ todo_list_as_data <-
+ Convertible.model_to_as(%{todo_list | actor: group}),
+ audience <- %{"to" => [group.members_url], "cc" => []},
+ update_data <-
+ make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
+ {:ok, todo_list, update_data}
+ end
+ end
+
+ @impl Entity
+ @spec delete(TodoList.t(), Actor.t(), boolean()) ::
+ {:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
+ def delete(
+ %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
+ %Actor{url: actor_url} = actor,
+ _local
+ ) do
+ Logger.debug("Building Delete TodoList activity")
+
+ activity_data = %{
+ "actor" => actor_url,
+ "type" => "Delete",
+ "object" => Convertible.model_to_as(url),
+ "id" => url <> "/delete",
+ "to" => [group_url]
+ }
+
+ with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list),
+ {:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do
+ {:ok, activity_data, actor, todo_list}
+ end
+ end
+
+ def actor(%TodoList{}), do: nil
+
+ def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
+end
diff --git a/lib/federation/activity_pub/types/todos.ex b/lib/federation/activity_pub/types/todos.ex
new file mode 100644
index 00000000..dcf635f5
--- /dev/null
+++ b/lib/federation/activity_pub/types/todos.ex
@@ -0,0 +1,80 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
+ @moduledoc false
+ alias Mobilizon.{Actors, Todos}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub.Types.Entity
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Todos.{Todo, TodoList}
+ import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
+ require Logger
+
+ @behaviour Entity
+
+ @impl Entity
+ @spec create(map(), map()) :: {:ok, map()}
+ def create(args, additional) do
+ with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
+ Todos.create_todo(args),
+ %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
+ %Actor{} = creator <- Actors.get_actor(creator_id),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
+ todo_as_data <-
+ Convertible.model_to_as(todo),
+ audience <- %{"to" => [group.members_url], "cc" => []},
+ create_data <-
+ make_create_data(todo_as_data, Map.merge(audience, additional)) do
+ {:ok, todo, create_data}
+ end
+ end
+
+ @impl Entity
+ @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
+ def update(%Todo{} = old_todo, args, additional) do
+ with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
+ %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
+ {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+ todo_as_data <-
+ Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
+ audience <- %{"to" => [group.members_url], "cc" => []},
+ update_data <-
+ make_update_data(todo_as_data, Map.merge(audience, additional)) do
+ {:ok, todo, update_data}
+ end
+ end
+
+ @impl Entity
+ @spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()}
+ def delete(
+ %Todo{url: url, creator: %Actor{url: group_url}} = todo,
+ %Actor{url: actor_url} = actor,
+ _local
+ ) do
+ Logger.debug("Building Delete Todo activity")
+
+ activity_data = %{
+ "actor" => actor_url,
+ "type" => "Delete",
+ "object" => Convertible.model_to_as(url),
+ "id" => url <> "/delete",
+ "to" => [group_url]
+ }
+
+ with {:ok, _todo} <- Todos.delete_todo(todo),
+ {:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do
+ {:ok, activity_data, actor, todo}
+ end
+ end
+
+ def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id)
+
+ def group_actor(%Todo{todo_list_id: todo_list_id}) do
+ case Todos.get_todo_list(todo_list_id) do
+ %TodoList{actor_id: group_id} ->
+ Actors.get_actor(group_id)
+
+ _ ->
+ nil
+ end
+ end
+end
diff --git a/lib/federation/activity_pub/types/tombstones.ex b/lib/federation/activity_pub/types/tombstones.ex
new file mode 100644
index 00000000..b3f36cb6
--- /dev/null
+++ b/lib/federation/activity_pub/types/tombstones.ex
@@ -0,0 +1,14 @@
+defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
+ @moduledoc false
+ alias Mobilizon.{Actors, Tombstone}
+ alias Mobilizon.Actors.Actor
+
+ def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
+
+ def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id),
+ do: Actors.get_actor(actor_id)
+
+ def actor(_), do: nil
+
+ def group_actor(_), do: nil
+end
diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex
index 7a8d1d86..d59422e5 100644
--- a/lib/federation/activity_pub/utils.ex
+++ b/lib/federation/activity_pub/utils.ex
@@ -8,13 +8,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Various ActivityPub related utils.
"""
+ alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
+ alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures
+ alias Mobilizon.Web.Endpoint
require Logger
@@ -114,6 +117,53 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_federate(_), do: :ok
+ @doc """
+ Applies to activities sent by group members from outside this instance to a group of this instance,
+ we then need to relay (`Announce`) the object to other members on other instances.
+ """
+ def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
+
+ def maybe_relay_if_group_activity(
+ %Activity{local: false, data: %{"object" => object}},
+ _attributed_to
+ )
+ when is_map(object) do
+ do_maybe_relay_if_group_activity(object, object["attributedTo"])
+ end
+
+ # When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well
+ def maybe_relay_if_group_activity(
+ %Activity{local: false, data: %{"object" => object}},
+ %Actor{url: attributed_to_url}
+ )
+ when is_binary(object) do
+ do_maybe_relay_if_group_activity(object, attributed_to_url)
+ end
+
+ def maybe_relay_if_group_activity(_, _), do: :ok
+
+ defp do_maybe_relay_if_group_activity(object, attributed_to) when not is_nil(attributed_to) do
+ id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}"
+
+ case Actors.get_local_group_by_url(attributed_to) do
+ %Actor{} = group ->
+ case ActivityPub.announce(group, object, id, true, false) do
+ {:ok, _activity, _object} ->
+ Logger.info("Forwarded activity to external members of the group")
+ :ok
+
+ _ ->
+ Logger.info("Failed to forward activity to external members of the group")
+ :error
+ end
+
+ _ ->
+ :ok
+ end
+ end
+
+ defp do_maybe_relay_if_group_activity(_, _), do: :ok
+
@spec remote_actors(list(String.t())) :: list(Actor.t())
def remote_actors(recipients) do
recipients
@@ -135,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
- def lazy_put_activity_defaults(map) do
+ def lazy_put_activity_defaults(%{"object" => _object} = map) do
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"])
%{map | "object" => object}
@@ -147,7 +197,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Adds an id and published date if they aren't there.
"""
- def lazy_put_object_defaults(map) do
+ def lazy_put_object_defaults(map) when is_map(map) do
Map.put_new_lazy(map, "published", &make_date/0)
end
@@ -175,25 +225,49 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Checks that an incoming AP object's actor matches the domain it came from.
+
+ Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
"""
+ def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
+ when not is_nil(actor) and actor != "" do
+ params = Map.delete(params, "attributedTo")
+ origin_check?(id, params)
+ end
+
def origin_check?(id, %{"attributedTo" => actor} = params) do
params = params |> Map.put("actor", actor) |> Map.delete("attributedTo")
origin_check?(id, params)
end
- def origin_check?(id, %{"actor" => actor} = params) when not is_nil(actor) do
- id_uri = URI.parse(id)
- actor_uri = URI.parse(get_actor(params))
-
- compare_uris?(actor_uri, id_uri)
+ def origin_check?(id, %{"actor" => actor} = params)
+ when not is_nil(actor) and is_list(actor) and length(actor) > 0 do
+ origin_check?(id, Map.put(params, "actor", hd(actor)))
end
- def origin_check?(_id, %{"actor" => nil}), do: false
+ def origin_check?(id, %{"actor" => actor} = params)
+ when not is_nil(actor) do
+ actor = get_actor(params)
+ Logger.debug("Performing origin check on #{id} and #{actor} URIs")
+ compare_origins?(id, actor)
+ end
- def origin_check?(_id, _data), do: false
+ def origin_check?(_id, %{"type" => type} = _params) when type in ["Actor", "Group"], do: true
+
+ def origin_check?(_id, %{"actor" => nil} = _args), do: false
+
+ def origin_check?(_id, _args), do: false
+
+ @spec compare_origins?(String.t(), String.t()) :: boolean()
+ def compare_origins?(url_1, url_2) when is_binary(url_1) and is_binary(url_2) do
+ uri_1 = URI.parse(url_1)
+ uri_2 = URI.parse(url_2)
+
+ compare_uris?(uri_1, uri_2)
+ end
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host
+ @spec origin_check_from_id?(String.t(), String.t()) :: boolean()
def origin_check_from_id?(id, other_id) when is_binary(other_id) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
@@ -201,9 +275,20 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
compare_uris?(id_uri, other_uri)
end
+ @spec origin_check_from_id?(String.t(), map()) :: boolean()
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id)
+ def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do
+ case Ownable.group_actor(object) do
+ %Actor{type: :Group, id: group_id} ->
+ Actors.is_member?(actor_id, group_id)
+
+ _ ->
+ false
+ end
+ end
+
@doc """
Save picture data from %Plug.Upload{} and return AS Link data.
"""
@@ -274,7 +359,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
activity_id,
public
)
- when type in ["Note", "Event", "ResourceCollection", "Document"] do
+ when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do
do_make_announce_data(
actor,
object_actor_url,
@@ -367,6 +452,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
"type" => "Create",
"to" => object["to"],
"cc" => object["cc"],
+ "attributedTo" => object["attributedTo"] || object["actor"],
"actor" => object["actor"],
"object" => object,
"published" => make_date(),
@@ -494,7 +580,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Sign a request with the instance Relay actor.
"""
- @spec sign_fetch_relay(List.t(), String.t(), String.t()) :: List.t()
+ @spec sign_fetch_relay(Enum.t(), String.t(), String.t()) :: Enum.t()
def sign_fetch_relay(headers, id, date) do
with %Actor{} = actor <- Relay.get_actor() do
sign_fetch(headers, actor, id, date)
@@ -504,7 +590,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Sign a request with an actor.
"""
- @spec sign_fetch(List.t(), Actor.t(), String.t(), String.t()) :: List.t()
+ @spec sign_fetch(Enum.t(), Actor.t(), String.t(), String.t()) :: Enum.t()
def sign_fetch(headers, actor, id, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ make_signature(actor, id, date)
@@ -516,7 +602,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Add the Date header to the request if we sign object fetches
"""
- @spec maybe_date_fetch(List.t(), String.t()) :: List.t()
+ @spec maybe_date_fetch(Enum.t(), String.t()) :: Enum.t()
def maybe_date_fetch(headers, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ [{:Date, date}]
@@ -524,4 +610,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
headers
end
end
+
+ def check_for_actor_key_rotation(%Actor{} = actor) do
+ if Actors.should_rotate_actor_key(actor) do
+ Actors.schedule_key_rotation(
+ actor,
+ Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
+ )
+ end
+
+ :ok
+ end
end
diff --git a/lib/federation/activity_pub/visibility.ex b/lib/federation/activity_pub/visibility.ex
index e63f9289..b8599e88 100644
--- a/lib/federation/activity_pub/visibility.ex
+++ b/lib/federation/activity_pub/visibility.ex
@@ -8,7 +8,7 @@ defmodule Mobilizon.Federation.ActivityPub.Visibility do
Utility functions related to content visibility
"""
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Federation.ActivityPub.Activity
diff --git a/lib/federation/activity_stream.ex b/lib/federation/activity_stream.ex
new file mode 100644
index 00000000..f6d7a813
--- /dev/null
+++ b/lib/federation/activity_stream.ex
@@ -0,0 +1,7 @@
+defmodule Mobilizon.Federation.ActivityStream do
+ @moduledoc """
+ The ActivityStream Type
+ """
+
+ @type t :: map()
+end
diff --git a/lib/federation/activity_stream/converter/actor.ex b/lib/federation/activity_stream/converter/actor.ex
index 39fd5dd3..54ad8bfa 100644
--- a/lib/federation/activity_stream/converter/actor.ex
+++ b/lib/federation/activity_stream/converter/actor.ex
@@ -49,7 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
- summary: data["summary"],
+ summary: data["summary"] || "",
keys: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
@@ -57,6 +57,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
followers_url: data["followers"],
members_url: data["members"],
resources_url: data["resources"],
+ todos_url: data["todos"],
+ events_url: data["events"],
+ posts_url: data["posts"],
+ discussions_url: data["discussions"],
shared_inbox_url: data["endpoints"]["sharedInbox"],
domain: URI.parse(data["id"]).host,
manually_approves_followers: data["manuallyApprovesFollowers"],
@@ -77,12 +81,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
"type" => actor.type,
"preferredUsername" => actor.preferred_username,
"name" => actor.name,
- "summary" => actor.summary,
+ "summary" => actor.summary || "",
"following" => actor.following_url,
"followers" => actor.followers_url,
"members" => actor.members_url,
"resources" => actor.resources_url,
"todos" => actor.todos_url,
+ "posts" => actor.posts_url,
+ "events" => actor.events_url,
+ "discussions" => actor.discussions_url,
"inbox" => actor.inbox_url,
"outbox" => actor.outbox_url,
"url" => actor.url,
diff --git a/lib/federation/activity_stream/converter/comment.ex b/lib/federation/activity_stream/converter/comment.ex
index a3310614..e5075c64 100644
--- a/lib/federation/activity_stream/converter/comment.ex
+++ b/lib/federation/activity_stream/converter/comment.ex
@@ -7,22 +7,30 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
"""
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment, as: CommentModel
+ alias Mobilizon.Discussions
+ alias Mobilizon.Discussions.Comment, as: CommentModel
+ alias Mobilizon.Discussions.Discussion
alias Mobilizon.Events.Event
- alias Mobilizon.Tombstone, as: TombstoneModel
-
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Visibility
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
- alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
+ alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
+ alias Mobilizon.Tombstone, as: TombstoneModel
+
+ import Mobilizon.Federation.ActivityStream.Converter.Utils,
+ only: [
+ fetch_tags: 1,
+ fetch_mentions: 1,
+ build_tags: 1,
+ build_mentions: 1,
+ maybe_fetch_actor_and_attributed_to_id: 1
+ ]
require Logger
@behaviour Converter
defimpl Convertible, for: CommentModel do
- alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
-
defdelegate model_to_as(comment), to: CommentConverter
end
@@ -35,61 +43,35 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object))
- with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
- {:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <-
- ActivityPub.get_or_fetch_actor_by_url(author_url),
- {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
+ with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
+ maybe_fetch_actor_and_attributed_to_id(object),
+ {:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))},
{:mentions, mentions} <-
- {:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do
+ {:mentions, fetch_mentions(Map.get(object, "tag", []))},
+ discussion <-
+ Discussions.get_discussion_by_url(Map.get(object, "context")) do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))
data = %{
text: object["content"],
url: object["id"],
+ # Will be used in conversations, ignored in basic comments
+ title: object["name"],
+ context: object["context"],
actor_id: actor_id,
+ attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
in_reply_to_comment_id: nil,
event_id: nil,
uuid: object["uuid"],
+ discussion_id: if(is_nil(discussion), do: nil, else: discussion.id),
tags: tags,
mentions: mentions,
- local: is_nil(domain),
+ local: is_nil(actor_domain),
visibility: if(Visibility.is_public?(object), do: :public, else: :private)
}
- # We fetch the parent object
- Logger.debug("We're fetching the parent object")
-
- if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
- object["inReplyTo"] != "" do
- Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
-
- case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
- # Reply to an event (Event)
- {:ok, %Event{id: id}} ->
- Logger.debug("Parent object is an event")
- data |> Map.put(:event_id, id)
-
- # Reply to a comment (Comment)
- {:ok, %CommentModel{id: id} = comment} ->
- Logger.debug("Parent object is another comment")
-
- data
- |> Map.put(:in_reply_to_comment_id, id)
- |> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
- |> Map.put(:event_id, comment.event_id)
-
- # Anything else is kind of a MP
- {:error, parent} ->
- Logger.warn("Parent object is something we don't handle")
- Logger.debug(inspect(parent))
- data
- end
- else
- Logger.debug("No parent object for this comment")
-
- data
- end
+ maybe_fetch_parent_object(object, data)
else
{:ok, %Actor{suspended: true}} ->
:error
@@ -102,10 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
- to =
- if comment.visibility == :public,
- do: ["https://www.w3.org/ns/activitystreams#Public"],
- else: [comment.actor.followers_url]
+ to = determine_to(comment)
object = %{
"type" => "Note",
@@ -114,13 +93,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
"content" => comment.text,
"mediaType" => "text/html",
"actor" => comment.actor.url,
- "attributedTo" => comment.actor.url,
+ "attributedTo" =>
+ if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
+ comment.actor.url,
"uuid" => comment.uuid,
"id" => comment.url,
- "tag" =>
- ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
+ "tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags)
}
+ object =
+ if comment.discussion_id,
+ do: Map.put(object, "context", comment.discussion.url),
+ else: object
+
cond do
comment.in_reply_to_comment ->
Map.put(object, "inReplyTo", comment.in_reply_to_comment.url)
@@ -133,15 +118,78 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
end
end
- @impl Converter
- @spec model_to_as(CommentModel.t()) :: map
@doc """
A "soft-deleted" comment is a tombstone
"""
+ @impl Converter
+ @spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do
Convertible.model_to_as(%TombstoneModel{
uri: comment.url,
inserted_at: comment.deleted_at
})
end
+
+ @spec determine_to(CommentModel.t()) :: [String.t()]
+ defp determine_to(%CommentModel{} = comment) do
+ cond do
+ not is_nil(comment.attributed_to) ->
+ [comment.attributed_to.url]
+
+ comment.visibility == :public ->
+ ["https://www.w3.org/ns/activitystreams#Public"]
+
+ true ->
+ [comment.actor.followers_url]
+ end
+ end
+
+ defp maybe_fetch_parent_object(object, data) do
+ # We fetch the parent object
+ Logger.debug("We're fetching the parent object")
+
+ if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
+ object["inReplyTo"] != "" do
+ Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
+
+ case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
+ # Reply to an event (Event)
+ {:ok, %Event{id: id}} ->
+ Logger.debug("Parent object is an event")
+ data |> Map.put(:event_id, id)
+
+ # Reply to a comment (Comment)
+ {:ok, %CommentModel{id: id} = comment} ->
+ Logger.debug("Parent object is another comment")
+
+ data
+ |> Map.put(:in_reply_to_comment_id, id)
+ |> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
+ |> Map.put(:event_id, comment.event_id)
+
+ # Reply to a discucssion (Discussion)
+ {:ok,
+ %Discussion{
+ id: discussion_id,
+ last_comment: %CommentModel{id: last_comment_id, origin_comment_id: origin_comment_id}
+ } = _discussion} ->
+ Logger.debug("Parent object is a discussion")
+
+ data
+ |> Map.put(:in_reply_to_comment_id, last_comment_id)
+ |> Map.put(:origin_comment_id, origin_comment_id)
+ |> Map.put(:discussion_id, discussion_id)
+
+ # Anything else is kind of a MP
+ {:error, parent} ->
+ Logger.warn("Parent object is something we don't handle")
+ Logger.debug(inspect(parent))
+ data
+ end
+ else
+ Logger.debug("No parent object for this comment")
+
+ data
+ end
+ end
end
diff --git a/lib/federation/activity_stream/converter/converter.ex b/lib/federation/activity_stream/converter/converter.ex
index 4398304f..73b1ca69 100644
--- a/lib/federation/activity_stream/converter/converter.ex
+++ b/lib/federation/activity_stream/converter/converter.ex
@@ -6,6 +6,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
one, and back.
"""
- @callback as_to_model_data(map) :: map
- @callback model_to_as(struct) :: map
+ @type model_data :: map()
+
+ @callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data()
+ @callback model_to_as(model :: struct()) :: ActivityStream.t()
end
diff --git a/lib/federation/activity_stream/converter/discussion.ex b/lib/federation/activity_stream/converter/discussion.ex
new file mode 100644
index 00000000..514e0dfa
--- /dev/null
+++ b/lib/federation/activity_stream/converter/discussion.ex
@@ -0,0 +1,63 @@
+defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
+ @moduledoc """
+ Comment converter.
+
+ This module allows to convert events from ActivityStream format to our own
+ internal one, and back.
+ """
+
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.Discussion
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
+ alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
+ alias Mobilizon.Storage.Repo
+
+ require Logger
+
+ @behaviour Converter
+
+ defimpl Convertible, for: Discussion do
+ defdelegate model_to_as(comment), to: DiscussionConverter
+ end
+
+ @doc """
+ Make an AS comment object from an existing `discussion` structure.
+ """
+ @impl Converter
+ @spec model_to_as(Discussion.t()) :: map
+ def model_to_as(%Discussion{} = discussion) do
+ discussion = Repo.preload(discussion, [:last_comment, :actor, :creator])
+
+ %{
+ "type" => "Note",
+ "to" => [discussion.actor.followers_url],
+ "cc" => [],
+ "name" => discussion.title,
+ "content" => discussion.last_comment.text,
+ "mediaType" => "text/html",
+ "actor" => discussion.creator.url,
+ "attributedTo" => discussion.actor.url,
+ "id" => discussion.url,
+ "context" => discussion.url
+ }
+ end
+
+ @impl Converter
+ @spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
+ def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do
+ with creator_url <- Map.get(object, "actor"),
+ {:ok, %Actor{id: creator_id, suspended: false}} <-
+ ActivityPub.get_or_fetch_actor_by_url(creator_url),
+ actor_url <- Map.get(object, "attributedTo"),
+ {:ok, %Actor{id: actor_id, suspended: false}} <-
+ ActivityPub.get_or_fetch_actor_by_url(actor_url) do
+ %{
+ title: name,
+ actor_id: actor_id,
+ creator_id: creator_id,
+ url: object["id"]
+ }
+ end
+ end
+end
diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex
index 7319752a..04dff326 100644
--- a/lib/federation/activity_stream/converter/event.ex
+++ b/lib/federation/activity_stream/converter/event.ex
@@ -12,11 +12,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Media.Picture
- alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter
- alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
+
+ import Mobilizon.Federation.ActivityStream.Converter.Utils,
+ only: [
+ fetch_tags: 1,
+ fetch_mentions: 1,
+ build_tags: 1,
+ maybe_fetch_actor_and_attributed_to_id: 1
+ ]
require Logger
@@ -34,16 +40,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
@impl Converter
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
def as_to_model_data(object) do
- Logger.debug("event as_to_model_data")
- Logger.debug(inspect(object))
-
- with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
- {:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <-
- {:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
+ with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
+ maybe_fetch_actor_and_attributed_to_id(object),
{:address, address_id} <-
{:address, get_address(object["location"])},
- {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
- {:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])},
+ {:tags, tags} <- {:tags, fetch_tags(object["tag"])},
+ {:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do
attachments =
@@ -67,6 +69,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
title: object["name"],
description: object["content"],
organizer_actor_id: actor_id,
+ attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
picture_id: picture_id,
begins_on: object["startTime"],
ends_on: object["endTime"],
@@ -108,7 +111,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"type" => "Event",
"to" => to,
"cc" => [],
- "attributedTo" => event.organizer_actor.url,
+ "attributedTo" =>
+ if(is_nil(event.attributed_to), do: nil, else: event.attributed_to.url) ||
+ event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
@@ -120,7 +125,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"startTime" => event.begins_on |> date_to_string(),
"joinMode" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(),
- "tag" => event.tags |> ConverterUtils.build_tags(),
+ "tag" => event.tags |> build_tags(),
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
"repliesModerationOption" => event.options.comment_moderation,
"commentsEnabled" => event.options.comment_moderation == :allow_all,
diff --git a/lib/federation/activity_stream/converter/flag.ex b/lib/federation/activity_stream/converter/flag.ex
index f3abb7a7..975b78a8 100644
--- a/lib/federation/activity_stream/converter/flag.ex
+++ b/lib/federation/activity_stream/converter/flag.ex
@@ -9,7 +9,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
"""
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations
+ alias Mobilizon.Discussions
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Reports.Report
@@ -92,7 +92,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))
end),
- comments <- Enum.map(comments, &Conversations.get_comment_from_url/1) do
+ comments <- Enum.map(comments, &Discussions.get_comment_from_url/1) do
%{
"reporter" => reporter,
"uri" => object["id"],
diff --git a/lib/federation/activity_stream/converter/picture.ex b/lib/federation/activity_stream/converter/picture.ex
index fd0af3ea..e5c666a6 100644
--- a/lib/federation/activity_stream/converter/picture.ex
+++ b/lib/federation/activity_stream/converter/picture.ex
@@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Picture do
actor_id
)
when is_bitstring(picture_url) do
- with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url, [], @http_options),
+ with {:ok, %{body: body}} <- Tesla.get(picture_url, opts: @http_options),
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
Upload.store(%{body: body, name: name}),
{:picture_exists, nil} <- {:picture_exists, Media.get_picture_by_url(url)} do
diff --git a/lib/federation/activity_stream/converter/post.ex b/lib/federation/activity_stream/converter/post.ex
new file mode 100644
index 00000000..41e715f9
--- /dev/null
+++ b/lib/federation/activity_stream/converter/post.ex
@@ -0,0 +1,70 @@
+defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
+ @moduledoc """
+ Post converter.
+
+ This module allows to convert posts from ActivityStream format to our own
+ internal one, and back.
+ """
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.Utils
+ alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
+ alias Mobilizon.Posts.Post
+ require Logger
+
+ @behaviour Converter
+
+ defimpl Convertible, for: Post do
+ alias Mobilizon.Federation.ActivityStream.Converter.Post, as: PostConverter
+
+ defdelegate model_to_as(post), to: PostConverter
+ end
+
+ @doc """
+ Convert an post struct to an ActivityStream representation
+ """
+ @impl Converter
+ @spec model_to_as(Post.t()) :: map
+ def model_to_as(
+ %Post{author: %Actor{url: actor_url}, attributed_to: %Actor{url: creator_url}} = post
+ ) do
+ %{
+ "type" => "Article",
+ "actor" => actor_url,
+ "id" => post.url,
+ "name" => post.title,
+ "content" => post.body,
+ "attributedTo" => creator_url,
+ "published" => post.publish_at || post.inserted_at
+ }
+ end
+
+ @doc """
+ Converts an AP object data to our internal data structure.
+ """
+ @impl Converter
+ @spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
+ def as_to_model_data(
+ %{"type" => "Article", "actor" => creator, "attributedTo" => group} = object
+ ) do
+ with {:ok, %Actor{id: attributed_to_id}} <- get_actor(group),
+ {:ok, %Actor{id: author_id}} <- get_actor(creator) do
+ %{
+ title: object["name"],
+ body: object["content"],
+ url: object["id"],
+ attributed_to_id: attributed_to_id,
+ author_id: author_id,
+ local: false,
+ publish_at: object["published"]
+ }
+ else
+ {:error, err} -> {:error, err}
+ err -> {:error, err}
+ end
+ end
+
+ @spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
+ defp get_actor(nil), do: {:error, "nil property found for actor data"}
+ defp get_actor(actor), do: actor |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url()
+end
diff --git a/lib/federation/activity_stream/converter/todo_list.ex b/lib/federation/activity_stream/converter/todo_list.ex
index ad075710..66c1776e 100644
--- a/lib/federation/activity_stream/converter/todo_list.ex
+++ b/lib/federation/activity_stream/converter/todo_list.ex
@@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
"type" => "TodoList",
"actor" => group_url,
"id" => todo_list.url,
- "title" => todo_list.title
+ "name" => todo_list.title
}
end
diff --git a/lib/federation/activity_stream/converter/tombstone.ex b/lib/federation/activity_stream/converter/tombstone.ex
index d0712286..642e2648 100644
--- a/lib/federation/activity_stream/converter/tombstone.ex
+++ b/lib/federation/activity_stream/converter/tombstone.ex
@@ -28,6 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
%{
"type" => "Tombstone",
"id" => tombstone.uri,
+ "actor" => tombstone.actor.url,
"deleted" => tombstone.inserted_at
}
end
diff --git a/lib/federation/activity_stream/converter/utils.ex b/lib/federation/activity_stream/converter/utils.ex
index a47cc6f7..0c296cdf 100644
--- a/lib/federation/activity_stream/converter/utils.ex
+++ b/lib/federation/activity_stream/converter/utils.ex
@@ -23,6 +23,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1)
end
+ def fetch_tags(_), do: []
+
@spec fetch_mentions([map()]) :: [map()]
def fetch_mentions(mentions) when is_list(mentions) do
Logger.debug("fetching mentions")
@@ -30,6 +32,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
end
+ def fetch_mentions(_), do: []
+
def fetch_address(%{id: id}) do
with {id, ""} <- Integer.parse(id), do: %{id: id}
end
@@ -38,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
address
end
- @spec build_tags([Tag.t()]) :: [Map.t()]
+ @spec build_tags([Tag.t()]) :: [map()]
def build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
@@ -111,4 +115,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
defp create_mention({_, mention}, acc) when is_map(mention) do
create_mention(mention, acc)
end
+
+ @spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
+ def maybe_fetch_actor_and_attributed_to_id(%{
+ "actor" => actor_url,
+ "attributedTo" => attributed_to_url
+ })
+ when is_nil(attributed_to_url) do
+ {fetch_actor(actor_url), nil}
+ end
+
+ @spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
+ def maybe_fetch_actor_and_attributed_to_id(%{
+ "actor" => actor_url,
+ "attributedTo" => attributed_to_url
+ })
+ when is_nil(actor_url) do
+ {fetch_actor(attributed_to_url), nil}
+ end
+
+ # Only when both actor and attributedTo fields are both filled is when we can return both
+ def maybe_fetch_actor_and_attributed_to_id(%{
+ "actor" => actor_url,
+ "attributedTo" => attributed_to_url
+ })
+ when actor_url != attributed_to_url do
+ with actor <- fetch_actor(actor_url),
+ attributed_to <- fetch_actor(attributed_to_url) do
+ {actor, attributed_to}
+ end
+ end
+
+ # If we only have attributedTo and no actor, take attributedTo as the actor
+ def maybe_fetch_actor_and_attributed_to_id(%{
+ "attributedTo" => attributed_to_url
+ }) do
+ {fetch_actor(attributed_to_url), nil}
+ end
+
+ def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil}
+
+ @spec fetch_actor(String.t()) :: Actor.t()
+ defp fetch_actor(actor_url) do
+ with {:ok, %Actor{suspended: false} = actor} <-
+ ActivityPub.get_or_fetch_actor_by_url(actor_url) do
+ actor
+ end
+ end
end
diff --git a/lib/federation/activity_stream/convertible.ex b/lib/federation/activity_stream/convertible.ex
index cf6e2dfc..776161b1 100644
--- a/lib/federation/activity_stream/convertible.ex
+++ b/lib/federation/activity_stream/convertible.ex
@@ -3,8 +3,9 @@ defprotocol Mobilizon.Federation.ActivityStream.Convertible do
Convertible protocol.
"""
- @type activity_streams :: map
+ @type t :: struct()
+ @type activity_streams :: map()
- @spec model_to_as(t) :: activity_streams
+ @spec model_to_as(t()) :: activity_streams()
def model_to_as(convertible)
end
diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex
index 1b939aeb..84b496a4 100644
--- a/lib/federation/web_finger/web_finger.ex
+++ b/lib/federation/web_finger/web_finger.ex
@@ -118,13 +118,15 @@ defmodule Mobilizon.Federation.WebFinger do
Logger.debug(inspect(address))
with false <- is_nil(domain),
- {:ok, %HTTPoison.Response{} = response} <-
- HTTPoison.get(
+ {:ok, %{} = response} <-
+ Tesla.get(
address,
- [Accept: "application/json, application/activity+json, application/jrd+json"],
- @http_options
+ headers: [
+ {"accept", "application/json, application/activity+json, application/jrd+json"}
+ ],
+ opts: @http_options
),
- %{status_code: status_code, body: body} when status_code in 200..299 <- response,
+ %{status: status, body: body} when status in 200..299 <- response,
{:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else
diff --git a/lib/graphql/api/comments.ex b/lib/graphql/api/comments.ex
index 16c9b5f3..1d7033db 100644
--- a/lib/graphql/api/comments.ex
+++ b/lib/graphql/api/comments.ex
@@ -3,8 +3,8 @@ defmodule Mobilizon.GraphQL.API.Comments do
API for Comments.
"""
- alias Mobilizon.Conversations.Comment
-
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
@@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
end
def update_comment(%Comment{} = comment, args) do
- ActivityPub.update(:comment, comment, args, true)
+ ActivityPub.update(comment, args, true)
end
@doc """
@@ -27,8 +27,8 @@ defmodule Mobilizon.GraphQL.API.Comments do
Deletes a comment from an actor
"""
- @spec delete_comment(Comment.t()) :: {:ok, Activity.t(), Comment.t()} | any
- def delete_comment(%Comment{} = comment) do
- ActivityPub.delete(comment, true)
+ @spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
+ def delete_comment(%Comment{} = comment, %Actor{} = actor) do
+ ActivityPub.delete(comment, actor, true)
end
end
diff --git a/lib/graphql/api/events.ex b/lib/graphql/api/events.ex
index fdbb92ae..a7ebbd75 100644
--- a/lib/graphql/api/events.ex
+++ b/lib/graphql/api/events.ex
@@ -34,7 +34,7 @@ defmodule Mobilizon.GraphQL.API.Events do
Map.update(args, :picture, nil, fn picture ->
process_picture(picture, organizer_actor)
end) do
- ActivityPub.update(:event, event, args, Map.get(args, :draft, false) == false)
+ ActivityPub.update(event, args, Map.get(args, :draft, false) == false)
end
end
@@ -43,8 +43,8 @@ defmodule Mobilizon.GraphQL.API.Events do
If the event is deleted by
"""
- def delete_event(%Event{} = event, federate \\ true) do
- ActivityPub.delete(event, federate)
+ def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
+ ActivityPub.delete(event, actor, federate)
end
defp process_picture(nil, _), do: nil
diff --git a/lib/graphql/api/groups.ex b/lib/graphql/api/groups.ex
index 315853ed..e813d004 100644
--- a/lib/graphql/api/groups.ex
+++ b/lib/graphql/api/groups.ex
@@ -19,8 +19,25 @@ defmodule Mobilizon.GraphQL.API.Groups do
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
{:existing_group, nil} <-
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
+ args <- args |> Map.put(:type, :Group),
{:ok, %Activity{} = activity, %Actor{} = group} <-
- ActivityPub.create(:group, args, true, %{"actor" => args.creator_actor.url}) do
+ ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do
+ {:ok, activity, group}
+ else
+ {:existing_group, _} ->
+ {:error, "A group with this name already exists"}
+
+ {:is_owned, nil} ->
+ {:error, "Actor id is not owned by authenticated user"}
+ end
+ end
+
+ @spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
+ def update_group(%{id: id} = args) do
+ with {:existing_group, {:ok, %Actor{type: :Group} = group}} <-
+ {:existing_group, Actors.get_group_by_actor_id(id)},
+ {:ok, %Activity{} = activity, %Actor{} = group} <-
+ ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do
{:ok, activity, group}
else
{:existing_group, _} ->
diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex
index 5ccb9eea..bef94265 100644
--- a/lib/graphql/resolvers/admin.ex
+++ b/lib/graphql/resolvers/admin.ex
@@ -9,7 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Config
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Relay
@@ -297,7 +297,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
with {:changes, true} <- {:changes, args != %{}},
%Actor{} = instance_actor <- Relay.get_actor(),
- {:ok, _activity, _actor} <- ActivityPub.update(:actor, instance_actor, args, true) do
+ {:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do
:ok
else
{:changes, false} ->
diff --git a/lib/graphql/resolvers/comment.ex b/lib/graphql/resolvers/comment.ex
index fa52dca7..6ccb7a45 100644
--- a/lib/graphql/resolvers/comment.ex
+++ b/lib/graphql/resolvers/comment.ex
@@ -3,9 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
Handles the comment-related GraphQL calls.
"""
- alias Mobilizon.{Actors, Admin, Conversations}
+ alias Mobilizon.{Actors, Admin, Discussions}
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment, as: CommentModel
+ alias Mobilizon.Discussions.Comment, as: CommentModel
alias Mobilizon.Users
alias Mobilizon.Users.User
@@ -14,7 +14,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
require Logger
def get_thread(_parent, %{id: thread_id}, _context) do
- {:ok, Conversations.get_thread_replies(thread_id)}
+ {:ok, Discussions.get_thread_replies(thread_id)}
end
def create_comment(
@@ -51,7 +51,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
%CommentModel{actor_id: comment_actor_id} = comment <-
- Mobilizon.Conversations.get_comment(comment_id),
+ Mobilizon.Discussions.get_comment(comment_id),
true <- actor_id === comment_actor_id,
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
{:ok, comment}
@@ -72,15 +72,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
- {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
+ {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
%CommentModel{deleted_at: nil} = comment <-
- Conversations.get_comment_with_preload(comment_id) do
+ Discussions.get_comment_with_preload(comment_id) do
cond do
{:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) ->
- do_delete_comment(comment)
+ do_delete_comment(comment, actor)
role in [:moderator, :administrator] ->
- with {:ok, res} <- do_delete_comment(comment),
+ with {:ok, res} <- do_delete_comment(comment, actor),
%Actor{} = actor <- Actors.get_actor(actor_id) do
Admin.log_action(actor, "delete", comment)
@@ -103,9 +103,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
{:error, "You are not allowed to delete a comment if not connected"}
end
- defp do_delete_comment(%CommentModel{} = comment) do
+ defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do
with {:ok, _, %CommentModel{} = comment} <-
- Comments.delete_comment(comment) do
+ Comments.delete_comment(comment, actor) do
{:ok, comment}
end
end
diff --git a/lib/graphql/resolvers/conversation.ex b/lib/graphql/resolvers/conversation.ex
deleted file mode 100644
index 0baab03b..00000000
--- a/lib/graphql/resolvers/conversation.ex
+++ /dev/null
@@ -1,110 +0,0 @@
-defmodule Mobilizon.GraphQL.Resolvers.Conversation do
- @moduledoc """
- Handles the group-related GraphQL calls.
- """
-
- alias Mobilizon.{Actors, Conversations, Users}
- alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Conversation, as: ConversationModel
- alias Mobilizon.Storage.Page
- alias Mobilizon.Users.User
-
- def find_conversations_for_actor(
- %Actor{id: group_id},
- _args,
- %{
- context: %{
- current_user: %User{} = user
- }
- }
- ) do
- with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
- {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
- {:ok, Conversations.find_conversations_for_actor(group_id)}
- else
- {:member, false} ->
- {:ok, %Page{total: 0, elements: []}}
- end
- end
-
- def find_conversations_for_actor(%Actor{}, _args, _resolution) do
- {:ok, %Page{total: 0, elements: []}}
- end
-
- def get_conversation(_parent, %{id: id}, _resolution) do
- {:ok, Conversations.get_conversation(id)}
- end
-
- def get_comments_for_conversation(
- %ConversationModel{id: conversation_id},
- %{page: page, limit: limit},
- _resolution
- ) do
- {:ok, Conversations.get_comments_for_conversation(conversation_id, page, limit)}
- end
-
- def create_conversation(
- _parent,
- %{title: title, text: text, actor_id: actor_id, creator_id: creator_id},
- _resolution
- ) do
- with {:ok, %ConversationModel{} = conversation} <-
- Conversations.create_conversation(%{
- title: title,
- text: text,
- actor_id: actor_id,
- creator_id: creator_id
- }) do
- {:ok, conversation}
- end
- end
-
- def reply_to_conversation(
- _parent,
- %{text: text, conversation_id: conversation_id},
- %{
- context: %{
- current_user: %User{} = user
- }
- }
- ) do
- with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
- {:no_conversation, %ConversationModel{} = conversation} <-
- {:no_conversation, Conversations.get_conversation(conversation_id)},
- {:ok, %ConversationModel{} = conversation} <-
- Conversations.reply_to_conversation(
- conversation,
- %{
- text: text,
- actor_id: actor_id
- }
- ) do
- {:ok, conversation}
- end
- end
-
- @spec update_conversation(map(), map(), map()) :: {:ok, ConversationModel.t()}
- def update_conversation(
- _parent,
- %{title: title, conversation_id: conversation_id},
- %{
- context: %{
- current_user: %User{} = user
- }
- }
- ) do
- with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
- {:no_conversation, %ConversationModel{creator_id: creator_id} = conversation} <-
- {:no_conversation, Conversations.get_conversation(conversation_id)},
- {:check_access, true} <- {:check_access, actor_id == creator_id},
- {:ok, %ConversationModel{} = conversation} <-
- Conversations.update_conversation(
- conversation,
- %{
- title: title
- }
- ) do
- {:ok, conversation}
- end
- end
-end
diff --git a/lib/graphql/resolvers/discussion.ex b/lib/graphql/resolvers/discussion.ex
new file mode 100644
index 00000000..cc0010df
--- /dev/null
+++ b/lib/graphql/resolvers/discussion.ex
@@ -0,0 +1,179 @@
+defmodule Mobilizon.GraphQL.Resolvers.Discussion do
+ @moduledoc """
+ Handles the group-related GraphQL calls.
+ """
+
+ alias Mobilizon.{Actors, Discussions, Users}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.{Comment, Discussion}
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Storage.Page
+ alias Mobilizon.Users.User
+
+ def find_discussions_for_actor(
+ %Actor{id: group_id},
+ _args,
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }
+ ) do
+ with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
+ {:ok, Discussions.find_discussions_for_actor(group_id)}
+ else
+ {:member, false} ->
+ {:ok, %Page{total: 0, elements: []}}
+ end
+ end
+
+ def find_discussions_for_actor(%Actor{}, _args, _resolution) do
+ {:ok, %Page{total: 0, elements: []}}
+ end
+
+ def get_discussion(_parent, %{id: id}, %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }) do
+ with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ %Discussion{actor_id: actor_id} = discussion <-
+ Discussions.get_discussion(id),
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
+ {:ok, discussion}
+ end
+ end
+
+ def get_discussion(_parent, %{slug: slug}, %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }) do
+ with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ %Discussion{actor_id: actor_id} = discussion <-
+ Discussions.get_discussion_by_slug(slug),
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
+ {:ok, discussion}
+ else
+ nil -> {:error, "No such discussion"}
+ end
+ end
+
+ def get_discussion(_parent, _args, _resolution),
+ do: {:error, "You need to be logged-in to access discussions"}
+
+ def get_comments_for_discussion(
+ %Discussion{id: discussion_id},
+ %{page: page, limit: limit},
+ _resolution
+ ) do
+ {:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)}
+ end
+
+ def create_discussion(
+ _parent,
+ %{title: title, text: text, actor_id: actor_id},
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }
+ ) do
+ with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
+ {:ok, _activity, %Discussion{} = discussion} <-
+ ActivityPub.create(
+ :discussion,
+ %{
+ title: title,
+ text: text,
+ actor_id: actor_id,
+ creator_id: creator_id,
+ attributed_to_id: actor_id
+ },
+ true
+ ) do
+ {:ok, discussion}
+ end
+ end
+
+ def reply_to_discussion(
+ _parent,
+ %{text: text, discussion_id: discussion_id},
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }
+ ) do
+ with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ {:no_discussion,
+ %Discussion{
+ actor_id: actor_id,
+ last_comment: %Comment{
+ id: last_comment_id,
+ origin_comment_id: origin_comment_id,
+ in_reply_to_comment_id: previous_in_reply_to_comment_id
+ }
+ } = _discussion} <-
+ {:no_discussion, Discussions.get_discussion(discussion_id)},
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
+ {:ok, _activity, %Discussion{} = discussion} <-
+ ActivityPub.create(
+ :discussion,
+ %{
+ text: text,
+ discussion_id: discussion_id,
+ actor_id: creator_id,
+ attributed_to_id: actor_id,
+ in_reply_to_comment_id: last_comment_id,
+ origin_comment_id:
+ origin_comment_id || previous_in_reply_to_comment_id || last_comment_id
+ },
+ true
+ ) do
+ {:ok, discussion}
+ end
+ end
+
+ @spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()}
+ def update_discussion(
+ _parent,
+ %{title: title, discussion_id: discussion_id},
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }
+ ) do
+ with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
+ {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
+ {:no_discussion, Discussions.get_discussion(discussion_id)},
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
+ {:ok, _activity, %Discussion{} = discussion} <-
+ ActivityPub.update(
+ discussion,
+ %{
+ title: title
+ }
+ ) do
+ {:ok, discussion}
+ end
+ end
+
+ def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }) do
+ with {:actor, %Actor{id: creator_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
+ {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
+ {:no_discussion, Discussions.get_discussion(discussion_id)},
+ {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
+ {:ok, _activity, %Discussion{} = discussion} <-
+ ActivityPub.delete(discussion, actor) do
+ {:ok, discussion}
+ end
+ end
+end
diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex
index 0bf80ee8..2e974a92 100644
--- a/lib/graphql/resolvers/event.ex
+++ b/lib/graphql/resolvers/event.ex
@@ -255,13 +255,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
) do
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
{actor_id, ""} <- Integer.parse(actor_id),
- {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
+ {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id) do
cond do
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
- do_delete_event(event)
+ do_delete_event(event, actor)
role in [:moderator, :administrator] ->
- with {:ok, res} <- do_delete_event(event, !is_local),
+ with {:ok, res} <- do_delete_event(event, actor, !is_local),
%Actor{} = actor <- Actors.get_actor(actor_id) do
Admin.log_action(actor, "delete", event)
@@ -284,8 +284,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, "You need to be logged-in to delete an event"}
end
- defp do_delete_event(event, federate \\ true) when is_boolean(federate) do
- with {:ok, _activity, event} <- API.Events.delete_event(event) do
+ defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true)
+ when is_boolean(federate) do
+ with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do
{:ok, %{id: event.id}}
end
end
diff --git a/lib/graphql/resolvers/group.ex b/lib/graphql/resolvers/group.ex
index 1e4fdb7b..b406fab4 100644
--- a/lib/graphql/resolvers/group.ex
+++ b/lib/graphql/resolvers/group.ex
@@ -80,7 +80,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
API.Groups.create_group(args) do
{:ok, group}
else
- {:error, err} when is_bitstring(err) ->
+ {:error, err} when is_binary(err) ->
{:error, err}
{:is_owned, nil} ->
@@ -92,6 +92,36 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:error, "You need to be logged-in to create a group"}
end
+ @doc """
+ Create a new group. The creator is automatically added as admin
+ """
+ def update_group(
+ _parent,
+ args,
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ }
+ ) do
+ with %Actor{} = updater_actor <- Users.get_actor_for_user(user),
+ args <- Map.put(args, :updater_actor, updater_actor),
+ {:ok, _activity, %Actor{type: :Group} = group} <-
+ API.Groups.update_group(args) do
+ {:ok, group}
+ else
+ {:error, err} when is_binary(err) ->
+ {:error, err}
+
+ {:is_owned, nil} ->
+ {:error, "Creator actor id is not owned by the current user"}
+ end
+ end
+
+ def update_group(_parent, _args, _resolution) do
+ {:error, "You need to be logged-in to update a group"}
+ end
+
@doc """
Delete an existing group
"""
diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex
index c29eb363..ead62878 100644
--- a/lib/graphql/resolvers/member.ex
+++ b/lib/graphql/resolvers/member.ex
@@ -16,14 +16,26 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
"""
def find_members_for_group(
%Actor{id: group_id} = group,
- _args,
+ %{page: page, limit: limit, roles: roles},
%{
context: %{current_user: %User{} = user}
} = _resolution
) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
- {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
- %Page{} = page <- Actors.list_members_for_group(group) do
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
+ roles =
+ case roles do
+ "" ->
+ []
+
+ roles ->
+ roles
+ |> String.split(",")
+ |> Enum.map(&String.downcase/1)
+ |> Enum.map(&String.to_existing_atom/1)
+ end
+
+ %Page{} = page = Actors.list_members_for_group(group, roles, page, limit)
{:ok, page}
else
{:member, false} ->
diff --git a/lib/graphql/resolvers/person.ex b/lib/graphql/resolvers/person.ex
index 472d8666..edc46fc0 100644
--- a/lib/graphql/resolvers/person.ex
+++ b/lib/graphql/resolvers/person.ex
@@ -129,7 +129,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:find_actor, Actors.get_actor(id)},
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
args <- save_attached_pictures(args),
- {:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do
+ {:ok, _activity, %Actor{} = actor} <- ActivityPub.update(actor, args, true) do
{:ok, actor}
else
{:find_actor, nil} ->
diff --git a/lib/graphql/resolvers/post.ex b/lib/graphql/resolvers/post.ex
new file mode 100644
index 00000000..5e1dfa2b
--- /dev/null
+++ b/lib/graphql/resolvers/post.ex
@@ -0,0 +1,198 @@
+defmodule Mobilizon.GraphQL.Resolvers.Post do
+ @moduledoc """
+ Handles the posts-related GraphQL calls
+ """
+
+ alias Mobilizon.{Actors, Posts, Users}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Posts.Post
+ alias Mobilizon.Storage.Page
+ alias Mobilizon.Users.User
+
+ require Logger
+
+ @public_accessible_visibilities [:public, :unlisted]
+
+ @doc """
+ Find posts for group.
+
+ Returns only if actor requesting is a member of the group
+ """
+ def find_posts_for_group(
+ %Actor{id: group_id} = group,
+ %{page: page, limit: limit} = args,
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ } = _resolution
+ ) do
+ with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
+ %Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
+ {:ok, page}
+ else
+ {:member, _} ->
+ find_posts_for_group(group, args, nil)
+ end
+ end
+
+ def find_posts_for_group(
+ %Actor{} = group,
+ %{page: page, limit: limit},
+ _resolution
+ ) do
+ with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do
+ {:ok, page}
+ end
+ end
+
+ def find_posts_for_group(
+ _group,
+ _args,
+ _resolution
+ ) do
+ {:ok, %Page{total: 0, elements: []}}
+ end
+
+ def get_post(
+ parent,
+ %{slug: slug},
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ } = _resolution
+ ) do
+ with {:current_actor, %Actor{id: actor_id}} <-
+ {:current_actor, Users.get_actor_for_user(user)},
+ {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
+ {:post, Posts.get_post_by_slug_with_preloads(slug)},
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
+ {:ok, post}
+ else
+ {:member, false} -> get_post(parent, %{slug: slug}, nil)
+ {:post, _} -> {:error, "No such post"}
+ end
+ end
+
+ def get_post(
+ _parent,
+ %{slug: slug},
+ _resolution
+ ) do
+ case {:post, Posts.get_post_by_slug_with_preloads(slug)} do
+ {:post, %Post{visibility: visibility, draft: false} = post}
+ when visibility in @public_accessible_visibilities ->
+ {:ok, post}
+
+ {:post, _} ->
+ {:error, "No such post"}
+ end
+ end
+
+ def get_post(_parent, _args, _resolution) do
+ {:error, "No such post"}
+ end
+
+ def create_post(
+ _parent,
+ %{attributed_to_id: group_id} = args,
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ } = _resolution
+ ) do
+ with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
+ {:ok, _, %Post{} = post} <-
+ ActivityPub.create(
+ :post,
+ args
+ |> Map.put(:author_id, actor_id)
+ |> Map.put(:attributed_to_id, group_id),
+ true,
+ %{}
+ ) do
+ {:ok, post}
+ else
+ {:own_check, _} ->
+ {:error, "Parent post doesn't match this group"}
+
+ {:member, _} ->
+ {:error, "Actor id is not member of group"}
+ end
+ end
+
+ def create_post(_parent, _args, _resolution) do
+ {:error, "You need to be logged-in to create posts"}
+ end
+
+ def update_post(
+ _parent,
+ %{id: id} = args,
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ } = _resolution
+ ) do
+ with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)},
+ %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+ {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
+ {:post, Posts.get_post_with_preloads(id)},
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
+ {:ok, _, %Post{} = post} <-
+ ActivityPub.update(post, args, true, %{}) do
+ {:ok, post}
+ else
+ {:uuid, :error} ->
+ {:error, "Post ID is not a valid ID"}
+
+ {:post, _} ->
+ {:error, "Post doesn't exist"}
+
+ {:member, _} ->
+ {:error, "Actor id is not member of group"}
+ end
+ end
+
+ def update_post(_parent, _args, _resolution) do
+ {:error, "You need to be logged-in to update posts"}
+ end
+
+ def delete_post(
+ _parent,
+ %{id: post_id},
+ %{
+ context: %{
+ current_user: %User{} = user
+ }
+ } = _resolution
+ ) do
+ with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)},
+ %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
+ {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
+ {:post, Posts.get_post_with_preloads(post_id)},
+ {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
+ {:ok, _, %Post{} = post} <-
+ ActivityPub.delete(post, actor) do
+ {:ok, post}
+ else
+ {:uuid, :error} ->
+ {:error, "Post ID is not a valid ID"}
+
+ {:post, _} ->
+ {:error, "Post doesn't exist"}
+
+ {:member, _} ->
+ {:error, "Actor id is not member of group"}
+ end
+ end
+
+ def delete_post(_parent, _args, _resolution) do
+ {:error, "You need to be logged-in to delete posts"}
+ end
+end
diff --git a/lib/graphql/resolvers/resource.ex b/lib/graphql/resolvers/resource.ex
index a72db2a4..5310de03 100644
--- a/lib/graphql/resolvers/resource.ex
+++ b/lib/graphql/resolvers/resource.ex
@@ -141,7 +141,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
- ActivityPub.update(:resource, resource, args, true, %{}) do
+ ActivityPub.update(resource, args, true, %{}) do
{:ok, resource}
else
{:resource, _} ->
@@ -165,12 +165,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
- with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+ with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
{:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
- ActivityPub.delete(resource) do
+ ActivityPub.delete(resource, actor) do
{:ok, resource}
else
{:resource, _} ->
diff --git a/lib/graphql/resolvers/tag.ex b/lib/graphql/resolvers/tag.ex
index a21eb742..82188c13 100644
--- a/lib/graphql/resolvers/tag.ex
+++ b/lib/graphql/resolvers/tag.ex
@@ -3,8 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
Handles the tag-related GraphQL calls
"""
- alias Mobilizon.Events
+ alias Mobilizon.{Events, Posts}
alias Mobilizon.Events.{Event, Tag}
+ alias Mobilizon.Posts.Post
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do
tags = Mobilizon.Events.list_tags(page, limit)
@@ -16,7 +17,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
Retrieve the list of tags for an event
"""
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
- {:ok, Mobilizon.Events.list_tags_for_event(id)}
+ {:ok, Events.list_tags_for_event(id)}
end
@doc """
@@ -24,10 +25,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
"""
def list_tags_for_event(%{url: url}, _args, _resolution) do
with %Event{id: event_id} <- Events.get_event_by_url(url) do
- {:ok, Mobilizon.Events.list_tags_for_event(event_id)}
+ {:ok, Events.list_tags_for_event(event_id)}
end
end
+ @doc """
+ Retrieve the list of tags for a post
+ """
+ def list_tags_for_post(%Post{id: id}, _args, _resolution) do
+ {:ok, Posts.list_tags_for_post(id)}
+ end
+
# @doc """
# Retrieve the list of related tags for a given tag ID
# """
@@ -42,7 +50,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
Retrieve the list of related tags for a parent tag
"""
def get_related_tags(%Tag{} = tag, _args, _resolution) do
- with tags <- Mobilizon.Events.list_tag_neighbors(tag) do
+ with tags <- Events.list_tag_neighbors(tag) do
{:ok, tags}
end
end
diff --git a/lib/graphql/resolvers/todos.ex b/lib/graphql/resolvers/todos.ex
index 9f6844b3..73d7ce73 100644
--- a/lib/graphql/resolvers/todos.ex
+++ b/lib/graphql/resolvers/todos.ex
@@ -211,7 +211,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <-
- ActivityPub.update(:todo, todo, args, true, %{}) do
+ ActivityPub.update(todo, args, true, %{}) do
{:ok, todo}
else
{:todo_list, _} ->
diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex
index 69414974..a440b333 100644
--- a/lib/graphql/resolvers/user.ex
+++ b/lib/graphql/resolvers/user.ex
@@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
alias Mobilizon.Actors.Actor
alias Mobilizon.Crypto
alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User}
@@ -417,7 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(moderator_user)},
%User{disabled: false} = user <- Users.get_user(user_id),
- {:ok, %User{}} <- do_delete_account(%User{} = user) do
+ {:ok, %User{}} <-
+ do_delete_account(%User{} = user, Relay.get_actor()) do
Admin.log_action(moderator_actor, "delete", user)
else
{:moderator_actor, nil} ->
@@ -432,7 +434,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:error, "You need to be logged-in to delete your account"}
end
- defp do_delete_account(%User{} = user) do
+ defp do_delete_account(%User{} = user, actor_performing \\ nil) do
with actors <- Users.get_actors_for_user(user),
activated <- not is_nil(user.confirmed_at),
# Detach actors from user
@@ -444,7 +446,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
# Launch a background job to delete actors
:ok <-
Enum.each(actors, fn actor ->
- ActivityPub.delete(actor, true)
+ actor_performing = actor_performing || actor
+ ActivityPub.delete(actor, actor_performing, true)
end),
# Delete user
{:ok, user} <- Users.delete_user(user, reserve_email: activated) do
diff --git a/lib/graphql/schema.ex b/lib/graphql/schema.ex
index 34fe0c30..545605d7 100644
--- a/lib/graphql/schema.ex
+++ b/lib/graphql/schema.ex
@@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema do
alias Mobilizon.{
Actors,
Addresses,
- Conversations,
+ Discussions,
Events,
Media,
Reports,
@@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema do
}
alias Mobilizon.Actors.{Actor, Follower, Member}
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.Schema
alias Mobilizon.Storage.Repo
@@ -34,10 +34,11 @@ defmodule Mobilizon.GraphQL.Schema do
import_types(Schema.Actors.PersonType)
import_types(Schema.Actors.GroupType)
import_types(Schema.Actors.ApplicationType)
- import_types(Schema.Conversations.CommentType)
- import_types(Schema.Conversations.ConversationType)
+ import_types(Schema.Discussions.CommentType)
+ import_types(Schema.Discussions.DiscussionType)
import_types(Schema.SearchType)
import_types(Schema.ResourceType)
+ import_types(Schema.PostType)
import_types(Schema.Todos.TodoListType)
import_types(Schema.Todos.TodoType)
import_types(Schema.ConfigType)
@@ -116,7 +117,7 @@ defmodule Mobilizon.GraphQL.Schema do
|> Dataloader.add_source(Actors, default_source)
|> Dataloader.add_source(Users, default_source)
|> Dataloader.add_source(Events, default_source)
- |> Dataloader.add_source(Conversations, Conversations.data())
+ |> Dataloader.add_source(Discussions, Discussions.data())
|> Dataloader.add_source(Addresses, default_source)
|> Dataloader.add_source(Media, default_source)
|> Dataloader.add_source(Reports, default_source)
@@ -148,8 +149,9 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:admin_queries)
import_fields(:todo_list_queries)
import_fields(:todo_queries)
- import_fields(:conversation_queries)
+ import_fields(:discussion_queries)
import_fields(:resource_queries)
+ import_fields(:post_queries)
import_fields(:statistics_queries)
end
@@ -170,8 +172,9 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:admin_mutations)
import_fields(:todo_list_mutations)
import_fields(:todo_mutations)
- import_fields(:conversation_mutations)
+ import_fields(:discussion_mutations)
import_fields(:resource_mutations)
+ import_fields(:post_mutations)
end
@desc """
@@ -179,5 +182,6 @@ defmodule Mobilizon.GraphQL.Schema do
"""
subscription do
import_fields(:person_subscriptions)
+ import_fields(:discussion_subscriptions)
end
end
diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex
index a74a7382..18f24af4 100644
--- a/lib/graphql/schema/actors/group.ex
+++ b/lib/graphql/schema/actors/group.ex
@@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
use Absinthe.Schema.Notation
- alias Mobilizon.GraphQL.Resolvers.{Conversation, Group, Member, Resource, Todos}
+ alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Post, Resource, Todos}
alias Mobilizon.GraphQL.Schema
import_types(Schema.Actors.MemberType)
@@ -46,9 +46,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A list of the events this actor has organized")
end
- field :conversations, :paginated_conversation_list do
- resolve(&Conversation.find_conversations_for_actor/3)
- description("A list of the conversations for this group")
+ field :discussions, :paginated_discussion_list do
+ resolve(&Discussion.find_discussions_for_actor/3)
+ description("A list of the discussions for this group")
end
field(:types, :group_type, description: "The type of group : Group, Community,…")
@@ -58,8 +58,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
)
field :members, :paginated_member_list do
+ arg(:page, :integer, default_value: 1)
+ arg(:limit, :integer, default_value: 10)
+ arg(:roles, :string, default_value: "")
resolve(&Member.find_members_for_group/3)
- description("List of group members")
+ description("A paginated list of group members")
end
field :resources, :paginated_resource_list do
@@ -69,6 +72,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the resources this group has")
end
+ field :posts, :paginated_post_list do
+ arg(:page, :integer, default_value: 1)
+ arg(:limit, :integer, default_value: 10)
+ resolve(&Post.find_posts_for_group/3)
+ description("A paginated list of the posts this group has")
+ end
+
field :todo_lists, :paginated_todo_list_list do
resolve(&Todos.find_todo_lists_for_group/3)
description("A paginated list of the todo lists this group has")
@@ -99,6 +109,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
field(:total, :integer, description: "The total number of elements in the list")
end
+ @desc "The list of visibility options for a group"
+ enum :group_visibility do
+ value(:public, description: "Publicly listed and federated")
+ value(:unlisted, description: "Visible only to people with the link - or invited")
+ end
+
object :group_queries do
@desc "Get all groups"
field :groups, :paginated_group_list do
@@ -124,6 +140,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
arg(:name, :string, description: "The displayed name for the group")
arg(:summary, :string, description: "The summary for the group", default_value: "")
+ arg(:visibility, :group_visibility,
+ description: "The visibility for the group",
+ default_value: :public
+ )
+
arg(:avatar, :picture_input,
description:
"The avatar for the group, either as an object or directly the ID of an existing Picture"
@@ -137,6 +158,26 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
resolve(&Group.create_group/3)
end
+ @desc "Update a group"
+ field :update_group, :group do
+ arg(:id, non_null(:id), description: "The group ID")
+
+ arg(:name, :string, description: "The displayed name for the group")
+ arg(:summary, :string, description: "The summary for the group", default_value: "")
+
+ arg(:avatar, :picture_input,
+ description:
+ "The avatar for the group, either as an object or directly the ID of an existing Picture"
+ )
+
+ arg(:banner, :picture_input,
+ description:
+ "The banner for the group, either as an object or directly the ID of an existing Picture"
+ )
+
+ resolve(&Group.update_group/3)
+ end
+
@desc "Delete a group"
field :delete_group, :deleted_object do
arg(:group_id, non_null(:id))
diff --git a/lib/graphql/schema/actors/member.ex b/lib/graphql/schema/actors/member.ex
index 89dcac77..c08e0dd9 100644
--- a/lib/graphql/schema/actors/member.ex
+++ b/lib/graphql/schema/actors/member.ex
@@ -15,6 +15,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field(:actor, :person, description: "Which profile is member of")
field(:role, :member_role_enum, description: "The role of this membership")
field(:invited_by, :person, description: "Who invited this member")
+ field(:inserted_at, :naive_datetime, description: "When was this member created")
end
enum :member_role_enum do
diff --git a/lib/graphql/schema/admin.ex b/lib/graphql/schema/admin.ex
index 2d85474a..c9f1695a 100644
--- a/lib/graphql/schema/admin.ex
+++ b/lib/graphql/schema/admin.ex
@@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
use Absinthe.Schema.Notation
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
diff --git a/lib/graphql/schema/conversations/conversation.ex b/lib/graphql/schema/conversations/conversation.ex
deleted file mode 100644
index 307a90fb..00000000
--- a/lib/graphql/schema/conversations/conversation.ex
+++ /dev/null
@@ -1,74 +0,0 @@
-defmodule Mobilizon.GraphQL.Schema.Conversations.ConversationType do
- @moduledoc """
- Schema representation for Conversation
- """
- use Absinthe.Schema.Notation
-
- import Absinthe.Resolution.Helpers, only: [dataloader: 1]
-
- alias Mobilizon.Actors
- alias Mobilizon.GraphQL.Resolvers.Conversation
-
- @desc "A conversation"
- object :conversation do
- field(:id, :id, description: "Internal ID for this conversation")
- field(:title, :string)
- field(:slug, :string)
- field(:last_comment, :comment)
-
- field :comments, :paginated_comment_list do
- arg(:page, :integer, default_value: 1)
- arg(:limit, :integer, default_value: 10)
- resolve(&Conversation.get_comments_for_conversation/3)
- description("The comments for the conversation")
- end
-
- field(:creator, :person, resolve: dataloader(Actors))
- field(:actor, :actor, resolve: dataloader(Actors))
- field(:inserted_at, :datetime)
- field(:updated_at, :datetime)
- end
-
- object :paginated_conversation_list do
- field(:elements, list_of(:conversation), description: "A list of conversation")
- field(:total, :integer, description: "The total number of comments in the list")
- end
-
- object :conversation_queries do
- @desc "Get a conversation"
- field :conversation, type: :conversation do
- arg(:id, non_null(:id))
- resolve(&Conversation.get_conversation/3)
- end
- end
-
- object :conversation_mutations do
- @desc "Create a conversation"
- field :create_conversation, type: :conversation do
- arg(:title, non_null(:string))
- arg(:text, non_null(:string))
- arg(:actor_id, non_null(:id))
- arg(:creator_id, non_null(:id))
-
- resolve(&Conversation.create_conversation/3)
- end
-
- field :reply_to_conversation, type: :conversation do
- arg(:conversation_id, non_null(:id))
- arg(:text, non_null(:string))
- resolve(&Conversation.reply_to_conversation/3)
- end
-
- field :update_conversation, type: :conversation do
- arg(:title, non_null(:string))
- arg(:conversation_id, non_null(:id))
- resolve(&Conversation.update_conversation/3)
- end
-
- field :delete_conversation, type: :conversation do
- arg(:conversation_id, non_null(:id))
-
- # resolve(&Conversation.delete_conversation/3)
- end
- end
-end
diff --git a/lib/graphql/schema/conversations/comment.ex b/lib/graphql/schema/discussions/comment.ex
similarity index 89%
rename from lib/graphql/schema/conversations/comment.ex
rename to lib/graphql/schema/discussions/comment.ex
index c6f7911e..8f85e331 100644
--- a/lib/graphql/schema/conversations/comment.ex
+++ b/lib/graphql/schema/discussions/comment.ex
@@ -1,4 +1,4 @@
-defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
+defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@moduledoc """
Schema representation for Comment
"""
@@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
- alias Mobilizon.{Actors, Conversations}
+ alias Mobilizon.{Actors, Discussions}
alias Mobilizon.GraphQL.Resolvers.Comment
@desc "A comment"
@@ -21,13 +21,13 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
field(:primaryLanguage, :string)
field(:replies, list_of(:comment)) do
- resolve(dataloader(Conversations))
+ resolve(dataloader(Discussions))
end
field(:total_replies, :integer)
- field(:in_reply_to_comment, :comment, resolve: dataloader(Conversations))
+ field(:in_reply_to_comment, :comment, resolve: dataloader(Discussions))
field(:event, :event, resolve: dataloader(Events))
- field(:origin_comment, :comment, resolve: dataloader(Conversations))
+ field(:origin_comment, :comment, resolve: dataloader(Discussions))
field(:threadLanguages, non_null(list_of(:string)))
field(:actor, :person, resolve: dataloader(Actors))
field(:inserted_at, :datetime)
diff --git a/lib/graphql/schema/discussions/discussion.ex b/lib/graphql/schema/discussions/discussion.ex
new file mode 100644
index 00000000..020339aa
--- /dev/null
+++ b/lib/graphql/schema/discussions/discussion.ex
@@ -0,0 +1,85 @@
+defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
+ @moduledoc """
+ Schema representation for discussion
+ """
+ use Absinthe.Schema.Notation
+
+ import Absinthe.Resolution.Helpers, only: [dataloader: 1]
+
+ alias Mobilizon.Actors
+ alias Mobilizon.GraphQL.Resolvers.Discussion
+
+ @desc "A discussion"
+ object :discussion do
+ field(:id, :id, description: "Internal ID for this discussion")
+ field(:title, :string)
+ field(:slug, :string)
+ field(:last_comment, :comment)
+
+ field :comments, :paginated_comment_list do
+ arg(:page, :integer, default_value: 1)
+ arg(:limit, :integer, default_value: 10)
+ resolve(&Discussion.get_comments_for_discussion/3)
+ description("The comments for the discussion")
+ end
+
+ field(:creator, :person, resolve: dataloader(Actors))
+ field(:actor, :actor, resolve: dataloader(Actors))
+ field(:inserted_at, :datetime)
+ field(:updated_at, :datetime)
+ end
+
+ object :paginated_discussion_list do
+ field(:elements, list_of(:discussion), description: "A list of discussion")
+ field(:total, :integer, description: "The total number of comments in the list")
+ end
+
+ object :discussion_queries do
+ @desc "Get a discussion"
+ field :discussion, type: :discussion do
+ arg(:id, :id)
+ arg(:slug, :string)
+ resolve(&Discussion.get_discussion/3)
+ end
+ end
+
+ object :discussion_mutations do
+ @desc "Create a discussion"
+ field :create_discussion, type: :discussion do
+ arg(:title, non_null(:string))
+ arg(:text, non_null(:string))
+ arg(:actor_id, non_null(:id))
+ arg(:creator_id, non_null(:id))
+
+ resolve(&Discussion.create_discussion/3)
+ end
+
+ field :reply_to_discussion, type: :discussion do
+ arg(:discussion_id, non_null(:id))
+ arg(:text, non_null(:string))
+ resolve(&Discussion.reply_to_discussion/3)
+ end
+
+ field :update_discussion, type: :discussion do
+ arg(:title, non_null(:string))
+ arg(:discussion_id, non_null(:id))
+ resolve(&Discussion.update_discussion/3)
+ end
+
+ field :delete_discussion, type: :discussion do
+ arg(:discussion_id, non_null(:id))
+
+ resolve(&Discussion.delete_discussion/3)
+ end
+ end
+
+ object :discussion_subscriptions do
+ field :discussion_comment_changed, :discussion do
+ arg(:slug, non_null(:string))
+
+ config(fn args, _ ->
+ {:ok, topic: args.slug}
+ end)
+ end
+ end
+end
diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex
index 9b93a94a..d6a0ec56 100644
--- a/lib/graphql/schema/event.ex
+++ b/lib/graphql/schema/event.ex
@@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
import Mobilizon.GraphQL.Helpers.Error
- alias Mobilizon.{Actors, Addresses, Conversations}
+ alias Mobilizon.{Actors, Addresses, Discussions}
alias Mobilizon.GraphQL.Resolvers.{Event, Picture, Tag}
alias Mobilizon.GraphQL.Schema
@@ -82,7 +82,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
)
field(:comments, list_of(:comment), description: "The comments in reply to the event") do
- resolve(dataloader(Conversations))
+ resolve(dataloader(Discussions))
end
# field(:tracks, list_of(:track))
diff --git a/lib/graphql/schema/post.ex b/lib/graphql/schema/post.ex
new file mode 100644
index 00000000..fdfb61bc
--- /dev/null
+++ b/lib/graphql/schema/post.ex
@@ -0,0 +1,91 @@
+defmodule Mobilizon.GraphQL.Schema.PostType do
+ @moduledoc """
+ Schema representation for Posts
+ """
+ use Absinthe.Schema.Notation
+ alias Mobilizon.GraphQL.Resolvers.{Post, Tag}
+
+ @desc "A post"
+ object :post do
+ field(:id, :id, description: "The post's ID")
+ field(:title, :string, description: "The post's title")
+ field(:slug, :string, description: "The post's slug")
+ field(:body, :string, description: "The post's body, as HTML")
+ field(:url, :string, description: "The post's URL")
+ field(:draft, :boolean, description: "Whether the post is a draft")
+ field(:author, :actor, description: "The post's author")
+ field(:attributed_to, :actor, description: "The post's group")
+ field(:visibility, :post_visibility, description: "The post's visibility")
+ field(:publish_at, :datetime, description: "When the post was published")
+ field(:inserted_at, :naive_datetime, description: "The post's creation date")
+ field(:updated_at, :naive_datetime, description: "The post's last update date")
+
+ field(:tags, list_of(:tag),
+ resolve: &Tag.list_tags_for_post/3,
+ description: "The post's tags"
+ )
+ end
+
+ object :paginated_post_list do
+ field(:elements, list_of(:post), description: "A list of posts")
+ field(:total, :integer, description: "The total number of posts in the list")
+ end
+
+ @desc "The list of visibility options for a post"
+ enum :post_visibility do
+ value(:public, description: "Publicly listed and federated. Can be shared.")
+ value(:unlisted, description: "Visible only to people with the link")
+ # value(:restricted, description: "Visible only after a moderator accepted")
+
+ value(:private,
+ description: "Visible only to people members of the group or followers of the person"
+ )
+ end
+
+ object :post_queries do
+ @desc "Get a post"
+ field :post, :post do
+ arg(:slug, non_null(:string))
+ resolve(&Post.get_post/3)
+ end
+ end
+
+ object :post_mutations do
+ @desc "Create a post"
+ field :create_post, :post do
+ arg(:attributed_to_id, non_null(:id))
+ arg(:title, non_null(:string))
+ arg(:body, :string)
+ arg(:draft, :boolean, default_value: false)
+ arg(:visibility, :post_visibility)
+ arg(:publish_at, :datetime)
+
+ arg(:tags, list_of(:string),
+ default_value: [],
+ description: "The list of tags associated to the post"
+ )
+
+ resolve(&Post.create_post/3)
+ end
+
+ @desc "Update a post"
+ field :update_post, :post do
+ arg(:id, non_null(:id))
+ arg(:title, :string)
+ arg(:body, :string)
+ arg(:attributed_to_id, :id)
+ arg(:draft, :boolean)
+ arg(:visibility, :post_visibility)
+ arg(:publish_at, :datetime)
+ arg(:tags, list_of(:string), description: "The list of tags associated to the post")
+
+ resolve(&Post.update_post/3)
+ end
+
+ @desc "Delete a post"
+ field :delete_post, :deleted_object do
+ arg(:id, non_null(:id))
+ resolve(&Post.delete_post/3)
+ end
+ end
+end
diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex
index c84b5d64..57f86609 100644
--- a/lib/mobilizon/actors/actor.ex
+++ b/lib/mobilizon/actors/actor.ex
@@ -9,7 +9,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.{Actors, Config, Crypto, Mention, Share}
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File
alias Mobilizon.Reports.{Note, Report}
@@ -27,6 +27,9 @@ defmodule Mobilizon.Actors.Actor do
following_url: String.t(),
followers_url: String.t(),
shared_inbox_url: String.t(),
+ resources_url: String.t(),
+ posts_url: String.t(),
+ events_url: String.t(),
type: ActorType.t(),
name: String.t(),
domain: String.t(),
@@ -62,6 +65,10 @@ defmodule Mobilizon.Actors.Actor do
:shared_inbox_url,
:following_url,
:followers_url,
+ :posts_url,
+ :events_url,
+ :todos_url,
+ :discussions_url,
:type,
:name,
:domain,
@@ -96,6 +103,10 @@ defmodule Mobilizon.Actors.Actor do
:followers_url,
:members_url,
:resources_url,
+ :posts_url,
+ :todos_url,
+ :events_url,
+ :discussions_url,
:name,
:summary,
:manually_approves_followers,
@@ -117,6 +128,7 @@ defmodule Mobilizon.Actors.Actor do
schema "actors" do
field(:url, :string)
+
field(:outbox_url, :string)
field(:inbox_url, :string)
field(:following_url, :string)
@@ -124,7 +136,11 @@ defmodule Mobilizon.Actors.Actor do
field(:shared_inbox_url, :string)
field(:members_url, :string)
field(:resources_url, :string)
+ field(:posts_url, :string)
+ field(:events_url, :string)
field(:todos_url, :string)
+ field(:discussions_url, :string)
+
field(:type, ActorType, default: :Person)
field(:name, :string)
field(:domain, :string, default: nil)
@@ -344,7 +360,8 @@ defmodule Mobilizon.Actors.Actor do
def build_url("relay", :page, _args),
do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode()
- def build_url(preferred_username, endpoint, args) when endpoint in [:page, :resources] do
+ def build_url(preferred_username, endpoint, args)
+ when endpoint in [:page, :resources, :posts, :discussions, :events, :todos] do
endpoint = if endpoint == :page, do: :actor, else: endpoint
Endpoint
@@ -353,7 +370,7 @@ defmodule Mobilizon.Actors.Actor do
end
def build_url(preferred_username, endpoint, args)
- when endpoint in [:outbox, :following, :followers, :members, :todos] do
+ when endpoint in [:outbox, :following, :followers, :members] do
Endpoint
|> Routes.activity_pub_url(endpoint, preferred_username, args)
|> URI.decode()
diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex
index 757b0eed..57118cc1 100644
--- a/lib/mobilizon/actors/actors.ex
+++ b/lib/mobilizon/actors/actors.ex
@@ -55,6 +55,8 @@ defmodule Mobilizon.Actors do
@public_visibility [:public, :unlisted]
@administrator_roles [:creator, :administrator]
+ @moderator_roles [:moderator] ++ @administrator_roles
+ @member_roles [:member] ++ @moderator_roles
@actor_preloads [:user, :organized_events, :comments]
@doc """
@@ -118,6 +120,17 @@ defmodule Mobilizon.Actors do
end
end
+ @doc """
+ New function to replace `Mobilizon.Actors.get_actor_by_url/1` with
+ better signature
+ """
+ @spec get_actor_by_url_2(String.t(), boolean) :: Actor.t() | nil
+ def get_actor_by_url_2(url, preload \\ false) do
+ Actor
+ |> Repo.get_by(url: url)
+ |> preload_followers(preload)
+ end
+
@doc """
Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to
preload the followers relation.
@@ -181,9 +194,17 @@ defmodule Mobilizon.Actors do
"""
@spec create_actor(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def create_actor(attrs \\ %{}) do
- %Actor{}
- |> Actor.changeset(attrs)
- |> Repo.insert()
+ type = Map.get(attrs, :type, :Person)
+
+ case type do
+ :Person ->
+ %Actor{}
+ |> Actor.changeset(attrs)
+ |> Repo.insert()
+
+ :Group ->
+ create_group(attrs)
+ end
end
@doc """
@@ -238,7 +259,8 @@ defmodule Mobilizon.Actors do
name: name,
summary: summary,
avatar: transform_media_file(avatar),
- banner: transform_media_file(banner)
+ banner: transform_media_file(banner),
+ last_refreshed_at: DateTime.utc_now()
]
],
conflict_target: [:url]
@@ -285,6 +307,7 @@ defmodule Mobilizon.Actors do
"""
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do
+ Logger.info("Going to delete actor #{actor.url}")
actor = Repo.preload(actor, @actor_preloads)
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
@@ -306,10 +329,18 @@ defmodule Mobilizon.Actors do
case Repo.transaction(multi) do
{:ok, %{actor: %Actor{} = actor}} ->
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
+ Logger.info("Deleted actor #{actor.url}")
{:ok, actor}
{:error, remove, error, _} when remove in [:remove_banner, :remove_avatar] ->
+ Logger.error("Error while deleting actor's banner or avatar")
+ Logger.error(inspect(error, pretty: true))
{:error, error}
+
+ err ->
+ Logger.error("Unknown error while deleting actor")
+ Logger.error(inspect(err, pretty: true))
+ {:error, err}
end
end
@@ -438,23 +469,47 @@ defmodule Mobilizon.Actors do
end
end
+ @spec get_local_group_by_url(String.t()) :: Actor.t()
+ def get_local_group_by_url(group_url) do
+ group_query()
+ |> where([q], q.url == ^group_url and is_nil(q.domain))
+ |> Repo.one()
+ end
+
+ @spec get_group_by_members_url(String.t()) :: Actor.t()
+ def get_group_by_members_url(members_url) do
+ group_query()
+ |> where([q], q.members_url == ^members_url)
+ |> Repo.one()
+ end
+
@doc """
Creates a group.
+
+ If the group is local, creates an admin actor as well from `creator_actor_id`.
"""
@spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def create_group(attrs \\ %{}) do
- with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <-
- Multi.new()
- |> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs))
- |> Multi.insert(:add_admin_member, fn %{insert_group: group} ->
- Member.changeset(%Member{}, %{
- parent_id: group.id,
- actor_id: attrs.creator_actor_id,
- role: :administrator
- })
- end)
- |> Repo.transaction() do
- {:ok, group}
+ local = Map.get(attrs, :local, true)
+
+ if local do
+ with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <-
+ Multi.new()
+ |> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs))
+ |> Multi.insert(:add_admin_member, fn %{insert_group: group} ->
+ Member.changeset(%Member{}, %{
+ parent_id: group.id,
+ actor_id: attrs.creator_actor_id,
+ role: :administrator
+ })
+ end)
+ |> Repo.transaction() do
+ {:ok, group}
+ end
+ else
+ %Actor{}
+ |> Actor.group_creation_changeset(attrs)
+ |> Repo.insert()
end
end
@@ -532,12 +587,7 @@ defmodule Mobilizon.Actors do
def is_member?(actor_id, parent_id) do
match?(
{:ok, %Member{}},
- get_member(actor_id, parent_id, [
- :member,
- :moderator,
- :administrator,
- :creator
- ])
+ get_member(actor_id, parent_id, @member_roles)
)
end
@@ -552,6 +602,20 @@ defmodule Mobilizon.Actors do
|> Repo.one()
end
+ @spec get_single_group_member_actor(integer() | String.t()) :: Actor.t() | nil
+ def get_single_group_member_actor(group_id) do
+ Member
+ |> where(
+ [m],
+ m.parent_id == ^group_id and m.role in [^:member, ^:moderator, ^:administrator, ^:creator]
+ )
+ |> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
+ |> where([_m, a], is_nil(a.domain))
+ |> limit(1)
+ |> select([_m, a], a)
+ |> Repo.one()
+ end
+
@doc """
Creates a member.
"""
@@ -616,25 +680,26 @@ defmodule Mobilizon.Actors do
@doc """
Returns the list of members for a group.
"""
- @spec list_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
- def list_members_for_group(%Actor{id: group_id, type: :Group}, page \\ nil, limit \\ nil) do
- group_id
- |> members_for_group_query()
- |> Page.build_page(page, limit)
- end
-
- @spec list_external_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
- def list_external_members_for_group(
+ @spec list_members_for_group(Actor.t(), list(atom()), integer | nil, integer | nil) :: Page.t()
+ def list_members_for_group(
%Actor{id: group_id, type: :Group},
+ roles \\ [],
page \\ nil,
limit \\ nil
) do
group_id
|> members_for_group_query()
- |> filter_external()
+ |> filter_member_role(roles)
|> Page.build_page(page, limit)
end
+ @spec list_external_actors_members_for_group(Actor.t()) :: list(Actor.t())
+ def list_external_actors_members_for_group(%Actor{id: group_id, type: :Group}) do
+ group_id
+ |> group_external_member_actor_query()
+ |> Repo.all()
+ end
+
@doc """
Returns the list of administrator members for a group.
"""
@@ -1141,6 +1206,26 @@ defmodule Mobilizon.Actors do
)
end
+ @spec group_external_member_actor_query(integer()) :: Ecto.Query.t()
+ defp group_external_member_actor_query(group_id) do
+ Member
+ |> where([m], m.parent_id == ^group_id)
+ |> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
+ |> where([_m, a], not is_nil(a.domain))
+ |> select([_m, a], a)
+ end
+
+ @spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t()
+ def filter_member_role(query, []), do: query
+
+ def filter_member_role(query, roles) when is_list(roles) do
+ where(query, [m], m.role in ^roles)
+ end
+
+ def filter_member_role(query, role) when is_atom(role) do
+ from(m in query, where: m.role == ^role)
+ end
+
@spec administrator_members_for_group_query(integer | String.t()) :: Ecto.Query.t()
defp administrator_members_for_group_query(group_id) do
from(
@@ -1296,13 +1381,22 @@ defmodule Mobilizon.Actors do
defp preload_followers(actor, true), do: Repo.preload(actor, [:followers])
defp preload_followers(actor, false), do: actor
- defp delete_actor_organized_events(%Actor{organized_events: organized_events}) do
+ defp delete_actor_organized_events(%Actor{organized_events: organized_events} = actor) do
res =
Enum.map(organized_events, fn event ->
event =
- Repo.preload(event, [:organizer_actor, :participants, :picture, :mentions, :comments])
+ Repo.preload(event, [
+ :organizer_actor,
+ :participants,
+ :picture,
+ :mentions,
+ :comments,
+ :attributed_to,
+ :tags,
+ :physical_address
+ ])
- ActivityPub.delete(event, false)
+ ActivityPub.delete(event, actor, false)
end)
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
@@ -1312,13 +1406,21 @@ defmodule Mobilizon.Actors do
end
end
- defp delete_actor_empty_comments(%Actor{comments: comments}) do
+ defp delete_actor_empty_comments(%Actor{comments: comments} = actor) do
res =
Enum.map(comments, fn comment ->
comment =
- Repo.preload(comment, [:actor, :mentions, :event, :in_reply_to_comment, :origin_comment])
+ Repo.preload(comment, [
+ :actor,
+ :mentions,
+ :event,
+ :in_reply_to_comment,
+ :origin_comment,
+ :attributed_to,
+ :tags
+ ])
- ActivityPub.delete(comment, false)
+ ActivityPub.delete(comment, actor, false)
end)
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex
index fbe38aeb..9789d4bd 100644
--- a/lib/mobilizon/config.ex
+++ b/lib/mobilizon/config.ex
@@ -119,7 +119,7 @@ defmodule Mobilizon.Config do
@spec instance_user_agent :: String.t()
def instance_user_agent,
- do: "#{instance_name()} #{instance_hostname()} - Mobilizon #{instance_version()}"
+ do: "#{instance_hostname()} - Mobilizon #{instance_version()}"
@spec instance_federating :: String.t()
def instance_federating, do: instance_config()[:federating]
diff --git a/lib/mobilizon/conversations/conversation.ex b/lib/mobilizon/conversations/conversation.ex
deleted file mode 100644
index 1a54271c..00000000
--- a/lib/mobilizon/conversations/conversation.ex
+++ /dev/null
@@ -1,53 +0,0 @@
-defmodule Mobilizon.Conversations.Conversation.TitleSlug do
- @moduledoc """
- Module to generate the slug for conversations
- """
- use EctoAutoslugField.Slug, from: :title, to: :slug
-end
-
-defmodule Mobilizon.Conversations.Conversation do
- @moduledoc """
- Represents a conversation
- """
-
- use Ecto.Schema
-
- import Ecto.Changeset
-
- alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
- alias Mobilizon.Conversations.Conversation.TitleSlug
-
- @type t :: %__MODULE__{
- creator: Actor.t(),
- actor: Actor.t(),
- title: String.t(),
- slug: String.t(),
- last_comment: Comment.t(),
- comments: list(Comment.t())
- }
-
- @required_attrs [:actor_id, :creator_id, :title, :last_comment_id]
- @optional_attrs []
- @attrs @required_attrs ++ @optional_attrs
-
- schema "conversations" do
- field(:title, :string)
- field(:slug, TitleSlug.Type)
- belongs_to(:creator, Actor)
- belongs_to(:actor, Actor)
- belongs_to(:last_comment, Comment)
- has_many(:comments, Comment, foreign_key: :conversation_id)
-
- timestamps(type: :utc_datetime)
- end
-
- @doc false
- @spec changeset(t, map) :: Ecto.Changeset.t()
- def changeset(%__MODULE__{} = conversation, attrs) do
- conversation
- |> cast(attrs, @attrs)
- |> validate_required(@required_attrs)
- |> TitleSlug.maybe_generate_slug()
- end
-end
diff --git a/lib/mobilizon/conversations/comment.ex b/lib/mobilizon/discussions/comment.ex
similarity index 94%
rename from lib/mobilizon/conversations/comment.ex
rename to lib/mobilizon/discussions/comment.ex
index 2132563f..bde06e66 100644
--- a/lib/mobilizon/conversations/comment.ex
+++ b/lib/mobilizon/discussions/comment.ex
@@ -1,4 +1,4 @@
-defmodule Mobilizon.Conversations.Comment do
+defmodule Mobilizon.Discussions.Comment do
@moduledoc """
Represents an actor comment (for instance on an event or on a group).
"""
@@ -8,7 +8,7 @@ defmodule Mobilizon.Conversations.Comment do
import Ecto.Changeset
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.{Comment, CommentVisibility, Conversation}
+ alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
alias Mobilizon.Events.{Event, Tag}
alias Mobilizon.Mention
@@ -42,7 +42,7 @@ defmodule Mobilizon.Conversations.Comment do
:attributed_to_id,
:deleted_at,
:local,
- :conversation_id
+ :discussion_id
]
@attrs @required_attrs ++ @optional_attrs
@@ -60,7 +60,7 @@ defmodule Mobilizon.Conversations.Comment do
belongs_to(:event, Event, foreign_key: :event_id)
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
- belongs_to(:conversation, Conversation)
+ belongs_to(:discussion, Discussion, type: :binary_id)
has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
@@ -69,7 +69,7 @@ defmodule Mobilizon.Conversations.Comment do
end
@doc """
- Returns the id of the first comment in the conversation.
+ Returns the id of the first comment in the discussion.
"""
@spec get_thread_id(t) :: integer
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
@@ -98,6 +98,7 @@ defmodule Mobilizon.Conversations.Comment do
|> change()
|> put_change(:text, nil)
|> put_change(:actor_id, nil)
+ |> put_change(:discussion_id, nil)
|> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second))
end
diff --git a/lib/mobilizon/discussions/discussion.ex b/lib/mobilizon/discussions/discussion.ex
new file mode 100644
index 00000000..8e06b329
--- /dev/null
+++ b/lib/mobilizon/discussions/discussion.ex
@@ -0,0 +1,102 @@
+defmodule Mobilizon.Discussions.Discussion.TitleSlug do
+ @moduledoc """
+ Module to generate the slug for discussions
+ """
+ use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
+
+ def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
+ [title, ShortUUID.encode!(id)]
+ |> Enum.join("-")
+ |> Slugger.slugify()
+ end
+
+ def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: ""
+end
+
+defmodule Mobilizon.Discussions.Discussion do
+ @moduledoc """
+ Represents a discussion
+ """
+
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Mobilizon.Actors
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.Comment
+ alias Mobilizon.Discussions.Discussion.TitleSlug
+ alias Mobilizon.Web.Endpoint
+ alias Mobilizon.Web.Router.Helpers, as: Routes
+
+ @type t :: %__MODULE__{
+ creator: Actor.t(),
+ actor: Actor.t(),
+ title: String.t(),
+ url: String.t(),
+ slug: String.t(),
+ last_comment: Comment.t(),
+ comments: list(Comment.t())
+ }
+
+ @required_attrs [:actor_id, :creator_id, :title, :last_comment_id, :url, :id]
+ @optional_attrs []
+ @attrs @required_attrs ++ @optional_attrs
+
+ @primary_key {:id, Ecto.UUID, autogenerate: true}
+
+ schema "discussions" do
+ field(:title, :string)
+ field(:slug, TitleSlug.Type)
+ field(:url, :string)
+ belongs_to(:creator, Actor)
+ belongs_to(:actor, Actor)
+ belongs_to(:last_comment, Comment)
+ has_many(:comments, Comment, foreign_key: :discussion_id)
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ @spec changeset(t, map) :: Ecto.Changeset.t()
+ def changeset(%__MODULE__{} = discussion, attrs) do
+ discussion
+ |> cast(attrs, @attrs)
+ |> maybe_generate_id()
+ |> validate_required([:title, :id])
+ |> TitleSlug.maybe_generate_slug()
+ |> TitleSlug.unique_constraint()
+ |> maybe_generate_url()
+ |> validate_required(@required_attrs)
+ end
+
+ defp maybe_generate_id(%Ecto.Changeset{} = changeset) do
+ case fetch_field(changeset, :id) do
+ res when res in [:error, {:data, nil}] ->
+ put_change(changeset, :id, Ecto.UUID.generate())
+
+ _ ->
+ changeset
+ end
+ end
+
+ @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
+ defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
+ with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
+ {changes, slug} when changes in [:changes, :data] <-
+ fetch_field(changeset, :slug),
+ {_changes, actor_id} <-
+ fetch_field(changeset, :actor_id),
+ %Actor{preferred_username: preferred_username} <-
+ Actors.get_actor(actor_id),
+ url <- generate_url(preferred_username, slug) do
+ put_change(changeset, :url, url)
+ else
+ _ -> changeset
+ end
+ end
+
+ @spec generate_url(String.t(), String.t()) :: String.t()
+ defp generate_url(preferred_username, slug),
+ do: Routes.page_url(Endpoint, :discussion, preferred_username, slug)
+end
diff --git a/lib/mobilizon/conversations/conversations.ex b/lib/mobilizon/discussions/discussions.ex
similarity index 68%
rename from lib/mobilizon/conversations/conversations.ex
rename to lib/mobilizon/discussions/discussions.ex
index 7bed61f6..d5671e2b 100644
--- a/lib/mobilizon/conversations/conversations.ex
+++ b/lib/mobilizon/discussions/discussions.ex
@@ -1,6 +1,6 @@
-defmodule Mobilizon.Conversations do
+defmodule Mobilizon.Discussions do
@moduledoc """
- The conversations context
+ The discussions context
"""
import EctoEnum
@@ -9,7 +9,7 @@ defmodule Mobilizon.Conversations do
alias Ecto.Changeset
alias Ecto.Multi
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.{Comment, Conversation}
+ alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Storage.{Page, Repo}
defenum(
@@ -42,10 +42,11 @@ defmodule Mobilizon.Conversations do
:origin_comment,
:replies,
:tags,
- :mentions
+ :mentions,
+ :discussion
]
- @conversation_preloads [
+ @discussion_preloads [
:last_comment,
:comments,
:creator,
@@ -231,21 +232,11 @@ defmodule Mobilizon.Conversations do
@doc """
Returns the list of public comments for the actor.
"""
- @spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) ::
- {:ok, [Comment.t()], integer}
+ @spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
- comments =
- actor_id
- |> public_comments_for_actor_query()
- |> Page.paginate(page, limit)
- |> Repo.all()
-
- count_comments =
- actor_id
- |> count_comments_query()
- |> Repo.one()
-
- {:ok, comments, count_comments}
+ actor_id
+ |> public_comments_for_actor_query()
+ |> Page.build_page(page, limit)
end
@doc """
@@ -263,10 +254,10 @@ defmodule Mobilizon.Conversations do
|> Repo.all()
end
- @spec get_comments_for_conversation(integer, integer | nil, integer | nil) :: Page.t()
- def get_comments_for_conversation(conversation_id, page \\ nil, limit \\ nil) do
+ @spec get_comments_for_discussion(integer, integer | nil, integer | nil) :: Page.t()
+ def get_comments_for_discussion(discussion_id, page \\ nil, limit \\ nil) do
Comment
- |> where([c], c.conversation_id == ^conversation_id)
+ |> where([c], c.discussion_id == ^discussion_id)
|> order_by(asc: :inserted_at)
|> Page.build_page(page, limit)
end
@@ -277,80 +268,114 @@ defmodule Mobilizon.Conversations do
@spec count_local_comments :: integer
def count_local_comments, do: Repo.one(count_local_comments_query())
- def get_conversation(conversation_id) do
- Conversation
- |> Repo.get(conversation_id)
- |> Repo.preload(@conversation_preloads)
+ def get_discussion(discussion_id) do
+ Discussion
+ |> Repo.get(discussion_id)
+ |> Repo.preload(@discussion_preloads)
end
- @spec find_conversations_for_actor(integer, integer | nil, integer | nil) :: Page.t()
- def find_conversations_for_actor(actor_id, page \\ nil, limit \\ nil) do
- Conversation
+ @spec get_discussion_by_url(String.t() | nil) :: Discussion.t() | nil
+ def get_discussion_by_url(nil), do: nil
+
+ def get_discussion_by_url(discussion_url) do
+ Discussion
+ |> Repo.get_by(url: discussion_url)
+ |> Repo.preload(@discussion_preloads)
+ end
+
+ def get_discussion_by_slug(discussion_slug) do
+ Discussion
+ |> Repo.get_by(slug: discussion_slug)
+ |> Repo.preload(@discussion_preloads)
+ end
+
+ @spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t()
+ def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do
+ Discussion
|> where([c], c.actor_id == ^actor_id)
- |> preload(^@conversation_preloads)
+ |> preload(^@discussion_preloads)
|> Page.build_page(page, limit)
end
@doc """
- Creates a conversation.
+ Creates a discussion.
"""
- @spec create_conversation(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
- def create_conversation(attrs \\ %{}) do
- with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <-
+ @spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
+ def create_discussion(attrs \\ %{}) do
+ with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <-
Multi.new()
|> Multi.insert(
:comment,
- Comment.changeset(%Comment{}, Map.merge(attrs, %{actor_id: attrs.creator_id}))
+ Comment.changeset(
+ %Comment{},
+ Map.merge(attrs, %{actor_id: attrs.creator_id, attributed_to_id: attrs.actor_id})
+ )
)
- |> Multi.insert(:conversation, fn %{comment: %Comment{id: comment_id}} ->
- Conversation.changeset(
- %Conversation{},
+ |> Multi.insert(:discussion, fn %{comment: %Comment{id: comment_id}} ->
+ Discussion.changeset(
+ %Discussion{},
Map.merge(attrs, %{last_comment_id: comment_id})
)
end)
- |> Multi.update(:comment_conversation, fn %{
- comment: %Comment{} = comment,
- conversation: %Conversation{
- id: conversation_id
- }
- } ->
- Changeset.change(comment, %{conversation_id: conversation_id})
+ |> Multi.update(:comment_discussion, fn %{
+ comment: %Comment{} = comment,
+ discussion: %Discussion{
+ id: discussion_id,
+ url: discussion_url
+ }
+ } ->
+ Changeset.change(comment, %{discussion_id: discussion_id, url: discussion_url})
end)
|> Repo.transaction() do
- {:ok, conversation}
+ {:ok, discussion}
end
end
- def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do
- with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <-
+ def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do
+ with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <-
Multi.new()
|> Multi.insert(
:comment,
- Comment.changeset(%Comment{}, Map.merge(attrs, %{conversation_id: conversation_id}))
+ Comment.changeset(
+ %Comment{},
+ Map.merge(attrs, %{
+ discussion_id: discussion_id,
+ actor_id: Map.get(attrs, :creator_id, attrs.actor_id)
+ })
+ )
)
- |> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} ->
- Conversation.changeset(
- conversation,
+ |> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} ->
+ Discussion.changeset(
+ discussion,
%{last_comment_id: comment_id}
)
end)
|> Repo.transaction() do
- # For some reason conversation is not updated
- {:ok, Map.put(conversation, :last_comment, comment)}
+ # Discussion is not updated
+ {:ok, Map.put(discussion, :last_comment, comment)}
end
end
@doc """
- Update a conversation. Only their title for now.
+ Update a discussion. Only their title for now.
"""
- @spec update_conversation(Conversation.t(), map()) ::
- {:ok, Conversation.t()} | {:error, Changeset.t()}
- def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do
- conversation
- |> Conversation.changeset(attrs)
+ @spec update_discussion(Discussion.t(), map()) ::
+ {:ok, Discussion.t()} | {:error, Changeset.t()}
+ def update_discussion(%Discussion{} = discussion, attrs \\ %{}) do
+ discussion
+ |> Discussion.changeset(attrs)
|> Repo.update()
end
+ @doc """
+ Delete a discussion.
+ """
+ @spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
+ def delete_discussion(%Discussion{} = discussion) do
+ discussion
+ |> Repo.delete()
+ end
+
defp public_comments_for_actor_query(actor_id) do
Comment
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
@@ -365,11 +390,6 @@ defmodule Mobilizon.Conversations do
|> preload_for_comment()
end
- @spec count_comments_query(integer) :: Ecto.Query.t()
- defp count_comments_query(actor_id) do
- from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)
- end
-
@spec count_local_comments_query :: Ecto.Query.t()
defp count_local_comments_query do
from(
@@ -382,6 +402,6 @@ defmodule Mobilizon.Conversations do
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
- # @spec preload_for_conversation(Ecto.Query.t()) :: Ecto.Query.t()
- # defp preload_for_conversation(query), do: preload(query, ^@conversation_preloads)
+ # @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t()
+ # defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads)
end
diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex
index d2893a90..913c730f 100644
--- a/lib/mobilizon/events/event.ex
+++ b/lib/mobilizon/events/event.ex
@@ -13,7 +13,7 @@ defmodule Mobilizon.Events.Event do
alias Mobilizon.{Addresses, Events, Media, Mention}
alias Mobilizon.Addresses.Address
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{
EventOptions,
diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex
index 22abdc9d..6f1f5832 100644
--- a/lib/mobilizon/events/event_options.ex
+++ b/lib/mobilizon/events/event_options.ex
@@ -7,7 +7,7 @@ defmodule Mobilizon.Events.EventOptions do
import Ecto.Changeset
- alias Mobilizon.Conversations.CommentModeration
+ alias Mobilizon.Discussions.CommentModeration
alias Mobilizon.Events.{
EventOffer,
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index f9f01e6e..a0e62344 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -380,24 +380,19 @@ defmodule Mobilizon.Events do
@doc """
Lists public events for the actor, with all associations loaded.
"""
- @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) ::
- {:ok, [Event.t()], integer}
- def list_public_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
- events =
- actor_id
- |> event_for_actor_query()
- |> filter_public_visibility()
- |> filter_draft()
- |> preload_for_event()
- |> Page.paginate(page, limit)
- |> Repo.all()
+ @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
+ def list_public_events_for_actor(actor, page \\ nil, limit \\ nil)
- events_count =
- actor_id
- |> count_events_for_actor_query()
- |> Repo.one()
+ def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit),
+ do: list_organized_events_for_group(group, page, limit)
- {:ok, events, events_count}
+ def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do
+ actor_id
+ |> event_for_actor_query()
+ |> filter_public_visibility()
+ |> filter_draft()
+ |> preload_for_event()
+ |> Page.build_page(page, limit)
end
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
@@ -1321,15 +1316,6 @@ defmodule Mobilizon.Events do
)
end
- @spec count_events_for_actor_query(integer | String.t()) :: Ecto.Query.t()
- defp count_events_for_actor_query(actor_id) do
- from(
- e in Event,
- select: count(e.id),
- where: e.organizer_actor_id == ^actor_id
- )
- end
-
@spec count_local_events_query :: Ecto.Query.t()
defp count_local_events_query do
from(e in Event, select: count(e.id), where: e.local == ^true)
diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex
index a0885d1f..1670e786 100644
--- a/lib/mobilizon/events/participant.ex
+++ b/lib/mobilizon/events/participant.ex
@@ -19,7 +19,7 @@ defmodule Mobilizon.Events.Participant do
url: String.t(),
event: Event.t(),
actor: Actor.t(),
- metadata: Map.t()
+ metadata: map()
}
@required_attrs [:url, :role, :event_id, :actor_id]
diff --git a/lib/mobilizon/mentions/mention.ex b/lib/mobilizon/mentions/mention.ex
index 4428e2e2..a1473d22 100644
--- a/lib/mobilizon/mentions/mention.ex
+++ b/lib/mobilizon/mentions/mention.ex
@@ -6,7 +6,7 @@ defmodule Mobilizon.Mention do
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Storage.Repo
diff --git a/lib/mobilizon/posts/post.ex b/lib/mobilizon/posts/post.ex
new file mode 100644
index 00000000..66d5a20e
--- /dev/null
+++ b/lib/mobilizon/posts/post.ex
@@ -0,0 +1,137 @@
+defmodule Mobilizon.Posts.Post.TitleSlug do
+ @moduledoc """
+ Module to generate the slug for posts
+ """
+ use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
+
+ def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
+ [title, ShortUUID.encode!(id)]
+ |> Enum.join("-")
+ |> Slugger.slugify()
+ end
+
+ def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: ""
+end
+
+defmodule Mobilizon.Posts.Post do
+ @moduledoc """
+ Module that represent Posts published by groups
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias Ecto.Changeset
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Events.Tag
+ alias Mobilizon.Media.Picture
+ alias Mobilizon.Posts.Post.TitleSlug
+ alias Mobilizon.Posts.PostVisibility
+ alias Mobilizon.Web.Endpoint
+ alias Mobilizon.Web.Router.Helpers, as: Routes
+
+ @type t :: %__MODULE__{
+ url: String.t(),
+ local: boolean,
+ slug: String.t(),
+ body: String.t(),
+ title: String.t(),
+ draft: boolean,
+ visibility: PostVisibility.t(),
+ publish_at: DateTime.t(),
+ author: Actor.t(),
+ attributed_to: Actor.t(),
+ picture: Picture.t(),
+ tags: [Tag.t()]
+ }
+
+ @primary_key {:id, Ecto.UUID, autogenerate: true}
+
+ schema "posts" do
+ field(:body, :string)
+ field(:draft, :boolean, default: false)
+ field(:local, :boolean, default: true)
+ field(:slug, TitleSlug.Type)
+ field(:title, :string)
+ field(:url, :string)
+ field(:publish_at, :utc_datetime)
+ field(:visibility, PostVisibility, default_value: :public)
+ belongs_to(:author, Actor)
+ belongs_to(:attributed_to, Actor)
+ belongs_to(:picture, Picture, on_replace: :update)
+ many_to_many(:tags, Tag, join_through: "posts_tags", on_replace: :delete)
+
+ timestamps()
+ end
+
+ @required_attrs [
+ :id,
+ :title,
+ :body,
+ :draft,
+ :slug,
+ :url,
+ :author_id,
+ :attributed_to_id
+ ]
+ @optional_attrs [:picture_id, :local, :publish_at, :visibility]
+ @attrs @required_attrs ++ @optional_attrs
+
+ @doc false
+ def changeset(%__MODULE__{} = post, attrs) do
+ post
+ |> cast(attrs, @attrs)
+ |> maybe_generate_id()
+ |> put_tags(attrs)
+ |> maybe_put_publish_date()
+ # Validate ID and title here because they're needed for slug
+ |> validate_required([:id, :title])
+ |> TitleSlug.maybe_generate_slug()
+ |> TitleSlug.unique_constraint()
+ |> maybe_generate_url()
+ |> validate_required(@required_attrs)
+ end
+
+ defp maybe_generate_id(%Ecto.Changeset{} = changeset) do
+ case fetch_field(changeset, :id) do
+ res when res in [:error, {:data, nil}] ->
+ put_change(changeset, :id, Ecto.UUID.generate())
+
+ _ ->
+ changeset
+ end
+ end
+
+ @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
+ defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
+ with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
+ {changes, id_and_slug} when changes in [:changes, :data] <-
+ fetch_field(changeset, :slug),
+ url <- generate_url(id_and_slug) do
+ put_change(changeset, :url, url)
+ else
+ _ -> changeset
+ end
+ end
+
+ @spec generate_url(String.t()) :: String.t()
+ defp generate_url(id_and_slug), do: Routes.page_url(Endpoint, :post, id_and_slug)
+
+ @spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
+ defp put_tags(changeset, %{"tags" => tags}),
+ do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
+
+ defp put_tags(changeset, %{tags: tags}),
+ do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
+
+ defp put_tags(changeset, _), do: changeset
+
+ defp process_tag(tag), do: Tag.changeset(%Tag{}, tag)
+
+ defp maybe_put_publish_date(%Changeset{} = changeset) do
+ publish_at =
+ if get_field(changeset, :draft, true) == false,
+ do: DateTime.utc_now() |> DateTime.truncate(:second),
+ else: nil
+
+ put_change(changeset, :publish_at, publish_at)
+ end
+end
diff --git a/lib/mobilizon/posts/posts.ex b/lib/mobilizon/posts/posts.ex
new file mode 100644
index 00000000..b30fae8b
--- /dev/null
+++ b/lib/mobilizon/posts/posts.ex
@@ -0,0 +1,135 @@
+defmodule Mobilizon.Posts do
+ @moduledoc """
+ The Posts context.
+ """
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Events.Tag
+ alias Mobilizon.Posts.Post
+ alias Mobilizon.Storage.{Page, Repo}
+
+ import Ecto.Query
+ require Logger
+
+ @post_preloads [:author, :attributed_to, :picture]
+
+ import EctoEnum
+
+ defenum(PostVisibility, :post_visibility, [
+ :public,
+ :unlisted,
+ :restricted,
+ :private
+ ])
+
+ @doc """
+ Returns the list of recent posts for a group
+ """
+ @spec get_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
+ def get_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
+ group_id
+ |> do_get_posts_for_group()
+ |> Page.build_page(page, limit)
+ end
+
+ @spec get_public_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
+ def get_public_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
+ group_id
+ |> do_get_posts_for_group()
+ |> where([p], p.visibility == ^:public and not p.draft)
+ |> Page.build_page(page, limit)
+ end
+
+ def do_get_posts_for_group(group_id) do
+ Post
+ |> where(attributed_to_id: ^group_id)
+ |> order_by(desc: :inserted_at)
+ |> preload([p], [:author, :attributed_to, :picture])
+ end
+
+ @doc """
+ Get a post by it's ID
+ """
+ @spec get_post(integer | String.t()) :: Post.t() | nil
+ def get_post(nil), do: nil
+ def get_post(id), do: Repo.get(Post, id)
+
+ @spec get_post_with_preloads(integer | String.t()) :: Post.t() | nil
+ def get_post_with_preloads(id) do
+ Post
+ |> Repo.get(id)
+ |> Repo.preload(@post_preloads)
+ end
+
+ @spec get_post_by_slug(String.t()) :: Post.t() | nil
+ def get_post_by_slug(nil), do: nil
+ def get_post_by_slug(slug), do: Repo.get_by(Post, slug: slug)
+
+ @spec get_post_by_slug_with_preloads(String.t()) :: Post.t() | nil
+ def get_post_by_slug_with_preloads(slug) do
+ Post
+ |> Repo.get_by(slug: slug)
+ |> Repo.preload(@post_preloads)
+ end
+
+ @doc """
+ Get a post by it's URL
+ """
+ @spec get_post_by_url(String.t()) :: Post.t() | nil
+ def get_post_by_url(url), do: Repo.get_by(Post, url: url)
+
+ @spec get_post_by_url_with_preloads(String.t()) :: Post.t() | nil
+ def get_post_by_url_with_preloads(url) do
+ Post
+ |> Repo.get_by(url: url)
+ |> Repo.preload(@post_preloads)
+ end
+
+ @doc """
+ Creates a post.
+ """
+ @spec create_post(map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
+ def create_post(attrs \\ %{}) do
+ %Post{}
+ |> Post.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a post.
+ """
+ @spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
+ def update_post(%Post{} = post, attrs) do
+ post
+ |> Repo.preload(:tags)
+ |> Post.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a post
+ """
+ @spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
+ def delete_post(%Post{} = post), do: Repo.delete(post)
+
+ @doc """
+ Returns the list of tags for the post.
+ """
+ @spec list_tags_for_post(integer | String.t()) :: [Tag.t()]
+ def list_tags_for_post(post_id) do
+ {:ok, uuid} = Ecto.UUID.dump(post_id)
+
+ uuid
+ |> tags_for_post_query()
+ |> Repo.all()
+ end
+
+ @spec tags_for_post_query(integer) :: Ecto.Query.t()
+ defp tags_for_post_query(post_id) do
+ from(
+ t in Tag,
+ join: p in "posts_tags",
+ on: t.id == p.tag_id,
+ where: p.post_id == ^post_id
+ )
+ end
+end
diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex
index 74561682..20e556a6 100644
--- a/lib/mobilizon/reports/report.ex
+++ b/lib/mobilizon/reports/report.ex
@@ -8,7 +8,7 @@ defmodule Mobilizon.Reports.Report do
import Ecto.Changeset
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Note, ReportStatus}
diff --git a/lib/mobilizon/resources/resources.ex b/lib/mobilizon/resources/resources.ex
index 5d7f24bc..8f6b2a6c 100644
--- a/lib/mobilizon/resources/resources.ex
+++ b/lib/mobilizon/resources/resources.ex
@@ -23,6 +23,7 @@ defmodule Mobilizon.Resources do
Resource
|> where(actor_id: ^group_id)
|> order_by(desc: :updated_at)
+ |> preload([r], [:actor, :creator])
|> Page.build_page(page, limit)
end
@@ -55,6 +56,7 @@ defmodule Mobilizon.Resources do
Resource
|> where([r], r.parent_id == ^resource_id)
|> order_by(asc: :type)
+ |> preload([r], [:actor, :creator])
|> Page.build_page(page, limit)
end
diff --git a/lib/mobilizon/todos/todos.ex b/lib/mobilizon/todos/todos.ex
index e903c5a3..8910581b 100644
--- a/lib/mobilizon/todos/todos.ex
+++ b/lib/mobilizon/todos/todos.ex
@@ -27,6 +27,7 @@ defmodule Mobilizon.Todos do
TodoList
|> where(actor_id: ^group_id)
|> order_by(desc: :updated_at)
+ |> preload([:actor])
|> Page.build_page(page, limit)
end
diff --git a/lib/mobilizon/users/setting.ex b/lib/mobilizon/users/setting.ex
index 1534ce21..a7fb1804 100644
--- a/lib/mobilizon/users/setting.ex
+++ b/lib/mobilizon/users/setting.ex
@@ -7,6 +7,15 @@ defmodule Mobilizon.Users.Setting do
import Ecto.Changeset
alias Mobilizon.Users.{NotificationPendingNotificationDelay, User}
+ @type t :: %__MODULE__{
+ timezone: String.t(),
+ notification_on_day: boolean,
+ notification_each_week: boolean,
+ notification_before_event: boolean,
+ notification_pending_participation: NotificationPendingNotificationDelay.t(),
+ user: User.t()
+ }
+
@required_attrs [:user_id]
@optional_attrs [
diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex
index 6d0f8d32..ec077d14 100644
--- a/lib/service/export/feed.ex
+++ b/lib/service/export/feed.ex
@@ -47,7 +47,7 @@ defmodule Mobilizon.Service.Export.Feed do
defp fetch_actor_event_feed(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name(name),
{:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)},
- {:ok, events, _count} <- Events.list_public_events_for_actor(actor) do
+ %Page{elements: events} <- Events.list_public_events_for_actor(actor) do
{:ok, build_actor_feed(actor, events)}
else
err ->
diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex
index b4ed5c17..f4b0d047 100644
--- a/lib/service/export/icalendar.ex
+++ b/lib/service/export/icalendar.ex
@@ -49,7 +49,8 @@ defmodule Mobilizon.Service.Export.ICalendar do
@spec export_public_actor(Actor.t()) :: String.t()
def export_public_actor(%Actor{} = actor) do
with true <- Actor.is_public_visibility(actor),
- {:ok, events, _} <- Events.list_public_events_for_actor(actor) do
+ %Page{elements: events} <-
+ Events.list_public_events_for_actor(actor) do
{:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
end
end
diff --git a/lib/service/geospatial/addok.ex b/lib/service/geospatial/addok.ex
index 199652cf..c31b68bc 100644
--- a/lib/service/geospatial/addok.ex
+++ b/lib/service/geospatial/addok.ex
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Addok do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -15,26 +15,18 @@ defmodule Mobilizon.Service.Geospatial.Addok do
@default_country Application.get_env(:mobilizon, __MODULE__) |> get_in([:default_country]) ||
"France"
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
"""
@spec geocode(String.t(), keyword()) :: list(Address.t())
def geocode(lon, lat, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
Logger.debug("Asking addok for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
process_data(features)
else
_ -> []
@@ -47,14 +39,11 @@ defmodule Mobilizon.Service.Geospatial.Addok do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:search, %{q: q}, options)
Logger.debug("Asking addok for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
process_data(features)
else
_ -> []
diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex
index 16abce2a..4377a07f 100644
--- a/lib/service/geospatial/google_maps.ex
+++ b/lib/service/geospatial/google_maps.ex
@@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
alias Mobilizon.Addresses.Address
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -28,11 +29,6 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
@api_key_missing_message "API Key required to use Google Maps"
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
@@ -43,12 +39,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
Logger.debug("Asking Google Maps for reverse geocode with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, [], @http_options),
- {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"results" => results, "status" => "OK"} <- body do
Enum.map(results, fn entry -> process_data(entry, options) end)
else
- {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
+ %{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
raise ArgumentError, message: to_string(error_message)
end
end
@@ -63,15 +58,14 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
Logger.debug("Asking Google Maps for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, [], @http_options),
- {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"results" => results, "status" => "OK"} <- body do
results |> Enum.map(fn entry -> process_data(entry, options) end)
else
- {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
+ %{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
raise ArgumentError, message: to_string(error_message)
- {:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} ->
+ %{"results" => [], "status" => "ZERO_RESULTS"} ->
[]
end
end
@@ -165,18 +159,17 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
Logger.debug("Asking Google Maps for details with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, [], @http_options),
- {:ok, %{"result" => %{"name" => name}, "status" => "OK"}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"result" => %{"name" => name}, "status" => "OK"} <- body do
name
else
- {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
+ %{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
raise ArgumentError, message: to_string(error_message)
- {:ok, %{"status" => "INVALID_REQUEST"}} ->
+ %{"status" => "INVALID_REQUEST"} ->
raise ArgumentError, message: "Invalid Request"
- {:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} ->
+ %{"results" => [], "status" => "ZERO_RESULTS"} ->
nil
end
end
diff --git a/lib/service/geospatial/map_quest.ex b/lib/service/geospatial/map_quest.ex
index edb59754..2e284665 100644
--- a/lib/service/geospatial/map_quest.ex
+++ b/lib/service/geospatial/map_quest.ex
@@ -10,8 +10,8 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -21,11 +21,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
@api_key_missing_message "API Key required to use MapQuest"
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
@@ -35,25 +30,21 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
api_key = Keyword.get(options, :api_key, @api_key)
limit = Keyword.get(options, :limit, 10)
open_data = Keyword.get(options, :open_data, true)
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
prefix = if open_data, do: "open", else: "www"
if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(
+ with {:ok, %{status: 200, body: body}} <-
+ BaseClient.get(
"https://#{prefix}.mapquestapi.com/geocoding/v1/reverse?key=#{api_key}&location=#{
lat
- },#{lon}&maxResults=#{limit}",
- headers,
- @http_options
+ },#{lon}&maxResults=#{limit}"
),
- {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
+ %{"results" => results, "info" => %{"statuscode" => 0}} <- body do
results |> Enum.map(&process_data/1)
else
- {:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
+ {:ok, %{status: 403, body: err}} ->
raise(ArgumentError, message: err)
end
end
@@ -64,8 +55,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
limit = Keyword.get(options, :limit, 10)
api_key = Keyword.get(options, :api_key, @api_key)
@@ -82,12 +71,11 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
Logger.debug("Asking MapQuest for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"results" => results, "info" => %{"statuscode" => 0}} <- body do
results |> Enum.map(&process_data/1)
else
- {:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
+ {:ok, %{status: 403, body: err}} ->
raise(ArgumentError, message: err)
end
end
diff --git a/lib/service/geospatial/mimirsbrunn.ex b/lib/service/geospatial/mimirsbrunn.ex
index b2c06293..729d76fc 100644
--- a/lib/service/geospatial/mimirsbrunn.ex
+++ b/lib/service/geospatial/mimirsbrunn.ex
@@ -8,8 +8,8 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -17,25 +17,17 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
"""
@spec geocode(number(), number(), keyword()) :: list(Address.t())
def geocode(lon, lat, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
Logger.debug("Asking Mimirsbrunn for reverse geocoding with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ {:ok, %{"features" => features}} <- Jason.decode(body) do
process_data(features)
else
_ -> []
@@ -48,14 +40,11 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:search, %{q: q}, options)
Logger.debug("Asking Mimirsbrunn for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ {:ok, %{"features" => features}} <- Jason.decode(body) do
process_data(features)
else
_ -> []
diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex
index c53230ef..ba1d535b 100644
--- a/lib/service/geospatial/nominatim.ex
+++ b/lib/service/geospatial/nominatim.ex
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -14,25 +14,17 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
@api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
"""
@spec geocode(String.t(), keyword()) :: list(Address.t())
def geocode(lon, lat, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
Logger.debug("Asking Nominatim for geocode with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
features |> process_data() |> Enum.filter(& &1)
else
_ -> []
@@ -45,14 +37,11 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:search, %{q: q}, options)
Logger.debug("Asking Nominatim for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
features |> process_data() |> Enum.filter(& &1)
else
_ -> []
diff --git a/lib/service/geospatial/pelias.ex b/lib/service/geospatial/pelias.ex
index 377ced94..969a5e46 100644
--- a/lib/service/geospatial/pelias.ex
+++ b/lib/service/geospatial/pelias.ex
@@ -6,8 +6,8 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -15,25 +15,17 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Pelias implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
"""
@spec geocode(number(), number(), keyword()) :: list(Address.t())
def geocode(lon, lat, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
Logger.debug("Asking Pelias for reverse geocoding with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ {:ok, %{"features" => features}} <- Jason.decode(body) do
process_data(features)
else
_ -> []
@@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:search, %{q: q}, options)
Logger.debug("Asking Pelias for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ {:ok, %{"features" => features}} <- Jason.decode(body) do
process_data(features)
else
_ -> []
diff --git a/lib/service/geospatial/photon.ex b/lib/service/geospatial/photon.ex
index 99194441..9790d382 100644
--- a/lib/service/geospatial/photon.ex
+++ b/lib/service/geospatial/photon.ex
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Photon do
"""
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Provider
+ alias Mobilizon.Service.HTTP.BaseClient
require Logger
@@ -13,11 +13,6 @@ defmodule Mobilizon.Service.Geospatial.Photon do
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
@impl Provider
@doc """
Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
@@ -26,14 +21,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do
"""
@spec geocode(number(), number(), keyword()) :: list(Address.t())
def geocode(lon, lat, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
Logger.debug("Asking photon for reverse geocoding with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
process_data(features)
else
_ -> []
@@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do
"""
@spec search(String.t(), keyword()) :: list(Address.t())
def search(q, options \\ []) do
- user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
- headers = [{"User-Agent", user_agent}]
url = build_url(:search, %{q: q}, options)
Logger.debug("Asking photon for addresses with #{url}")
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
- HTTPoison.get(url, headers, @http_options),
- {:ok, %{"features" => features}} <- Poison.decode(body) do
+ with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
+ %{"features" => features} <- body do
process_data(features)
else
_ -> []
diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex
index a86fe2b3..20afc49c 100644
--- a/lib/service/geospatial/provider.ex
+++ b/lib/service/geospatial/provider.ex
@@ -15,7 +15,6 @@ defmodule Mobilizon.Service.Geospatial.Provider do
## Shared options
- * `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"` or `Mobilizon.Config.instance_user_agent/0`
* `:lang` Lang in which to prefer results. Used as a request parameter or
through an `Accept-Language` HTTP header. Defaults to `"en"`.
* `:country_code` An ISO 3166 country code. String or `nil`
diff --git a/lib/service/http/activity_pub.ex b/lib/service/http/activity_pub.ex
new file mode 100644
index 00000000..0d0f6483
--- /dev/null
+++ b/lib/service/http/activity_pub.ex
@@ -0,0 +1,38 @@
+defmodule Mobilizon.Service.HTTP.ActivityPub do
+ @moduledoc """
+ Tesla HTTP Client that is preconfigured to get and post ActivityPub content
+ """
+
+ alias Mobilizon.Config
+
+ @adapter Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney
+ @default_opts [
+ recv_timeout: 20_000
+ ]
+ @user_agent Config.instance_user_agent()
+
+ def client(options \\ []) do
+ headers = Keyword.get(options, :headers, [])
+ opts = Keyword.merge(@default_opts, Keyword.get(options, :opts, []))
+
+ middleware = [
+ {Tesla.Middleware.Headers,
+ [{"User-Agent", @user_agent}, {"Accept", "application/activity+json"}] ++ headers},
+ Tesla.Middleware.FollowRedirects,
+ {Tesla.Middleware.Timeout, timeout: 10_000},
+ {Tesla.Middleware.JSON, decode_content_types: "application/activity+json"}
+ ]
+
+ adapter = {@adapter, opts}
+
+ Tesla.client(middleware, adapter)
+ end
+
+ def get(client, url) do
+ Tesla.get(client, url)
+ end
+
+ def post(client, url, data) do
+ Tesla.post(client, url, data)
+ end
+end
diff --git a/lib/service/http/base_client.ex b/lib/service/http/base_client.ex
new file mode 100644
index 00000000..689ce9b9
--- /dev/null
+++ b/lib/service/http/base_client.ex
@@ -0,0 +1,30 @@
+defmodule Mobilizon.Service.HTTP.BaseClient do
+ @moduledoc """
+ Tesla HTTP Basic Client
+ """
+
+ use Tesla
+ alias Mobilizon.Config
+
+ @default_opts [
+ recv_timeout: 20_000
+ ]
+
+ adapter(Tesla.Adapter.Hackney, @default_opts)
+
+ @user_agent Config.instance_user_agent()
+
+ plug(Tesla.Middleware.FollowRedirects)
+
+ plug(Tesla.Middleware.Timeout, timeout: 10_000)
+
+ plug(Tesla.Middleware.Headers, [{"User-Agent", @user_agent}])
+
+ def get(url) do
+ get(url)
+ end
+
+ def post(url, data) do
+ post(url, data)
+ end
+end
diff --git a/lib/service/metadata/comment.ex b/lib/service/metadata/comment.ex
index fb2df5e5..99c89364 100644
--- a/lib/service/metadata/comment.ex
+++ b/lib/service/metadata/comment.ex
@@ -1,6 +1,6 @@
-defimpl Mobilizon.Service.Metadata, for: Mobilizon.Conversations.Comment do
+defimpl Mobilizon.Service.Metadata, for: Mobilizon.Discussions.Comment do
alias Phoenix.HTML.Tag
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
def build_tags(%Comment{} = comment, _locale \\ "en") do
[
diff --git a/lib/service/metadata/event.ex b/lib/service/metadata/event.ex
index 4781ab27..4c4d2504 100644
--- a/lib/service/metadata/event.ex
+++ b/lib/service/metadata/event.ex
@@ -2,10 +2,9 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
alias Phoenix.HTML
alias Phoenix.HTML.Tag
alias Mobilizon.Events.Event
- alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter
alias Mobilizon.Web.JsonLD.ObjectView
alias Mobilizon.Web.MediaProxy
- import Mobilizon.Web.Gettext
+ import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1]
def build_tags(%Event{} = event, locale \\ "en") do
event = Map.put(event, :description, process_description(event.description, locale))
@@ -41,24 +40,10 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
]
end
- defp process_description(nil, locale), do: process_description("", locale)
-
- defp process_description("", locale) do
- Gettext.put_locale(locale)
- gettext("The event organizer didn't add any description.")
- end
-
- defp process_description(description, _locale) do
- description
- |> HTMLFormatter.strip_tags()
- |> String.slice(0..200)
- |> (&"#{&1}…").()
- end
-
# Insert JSON-LD schema by hand because Tag.content_tag wants to escape it
defp json(%Event{title: title} = event) do
"event.json"
- |> ObjectView.render(%{event: %{event | title: HTMLFormatter.strip_tags(title)}})
+ |> ObjectView.render(%{event: %{event | title: strip_tags(title)}})
|> Jason.encode!()
end
end
diff --git a/lib/service/metadata/post.ex b/lib/service/metadata/post.ex
new file mode 100644
index 00000000..6553b78f
--- /dev/null
+++ b/lib/service/metadata/post.ex
@@ -0,0 +1,34 @@
+defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do
+ alias Phoenix.HTML
+ alias Phoenix.HTML.Tag
+ alias Mobilizon.Posts.Post
+ alias Mobilizon.Web.JsonLD.ObjectView
+ import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1]
+
+ def build_tags(%Post{} = post, locale \\ "en") do
+ post = Map.put(post, :body, process_description(post.body, locale))
+
+ tags = [
+ Tag.tag(:meta, property: "og:title", content: post.title),
+ Tag.tag(:meta, property: "og:url", content: post.url),
+ Tag.tag(:meta, property: "og:description", content: post.body),
+ Tag.tag(:meta, property: "og:type", content: "article"),
+ Tag.tag(:meta, property: "twitter:card", content: "summary"),
+ # Tell Search Engines what's the origin
+ Tag.tag(:link, rel: "canonical", href: post.url)
+ ]
+
+ tags ++
+ [
+ Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
+ ~s{} |> HTML.raw()
+ ]
+ end
+
+ # Insert JSON-LD schema by hand because Tag.content_tag wants to escape it
+ defp json(%Post{title: title} = post) do
+ "post.json"
+ |> ObjectView.render(%{post: %{post | title: strip_tags(title)}})
+ |> Jason.encode!()
+ end
+end
diff --git a/lib/service/metadata/utils.ex b/lib/service/metadata/utils.ex
index 027a7833..f2c25bdf 100644
--- a/lib/service/metadata/utils.ex
+++ b/lib/service/metadata/utils.ex
@@ -3,10 +3,34 @@ defmodule Mobilizon.Service.Metadata.Utils do
Tools to convert tags to string.
"""
+ alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter
alias Phoenix.HTML
+ import Mobilizon.Web.Gettext
+ @slice_limit 200
+
+ @spec stringify_tags(Enum.t()) :: String.t()
def stringify_tags(tags), do: Enum.reduce(tags, "", &stringify_tag/2)
defp stringify_tag(tag, acc) when is_tuple(tag), do: acc <> HTML.safe_to_string(tag)
defp stringify_tag(tag, acc) when is_binary(tag), do: acc <> tag
+
+ @spec strip_tags(String.t()) :: String.t()
+ def strip_tags(text), do: HTMLFormatter.strip_tags(text)
+
+ @spec process_description(String.t(), String.t(), integer()) :: String.t()
+ def process_description(description, locale \\ "en", limit \\ @slice_limit)
+ def process_description(nil, locale, limit), do: process_description("", locale, limit)
+
+ def process_description("", locale, _limit) do
+ Gettext.put_locale(locale)
+ gettext("The event organizer didn't add any description.")
+ end
+
+ def process_description(description, _locale, limit) do
+ description
+ |> HTMLFormatter.strip_tags()
+ |> String.slice(0..limit)
+ |> (&"#{&1}…").()
+ end
end
diff --git a/lib/service/rich_media/favicon.ex b/lib/service/rich_media/favicon.ex
index 756501bf..12768b2d 100644
--- a/lib/service/rich_media/favicon.ex
+++ b/lib/service/rich_media/favicon.ex
@@ -16,24 +16,24 @@ defmodule Mobilizon.Service.RichMedia.Favicon do
ssl: [{:versions, [:"tlsv1.2"]}]
]
- @spec fetch(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
+ @spec fetch(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
def fetch(url, options \\ []) do
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
headers = [{"User-Agent", user_agent}]
- case HTTPoison.get(url, headers, @options) do
- {:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 ->
+ case Tesla.get(url, headers: headers, opts: @options) do
+ {:ok, %{status: code, body: body}} when code in 200..299 ->
find_favicon_url(url, body, headers)
- {:ok, %HTTPoison.Response{}} ->
+ {:ok, %{}} ->
{:error, "Error while fetching the page"}
- {:error, %HTTPoison.Error{reason: reason}} ->
- {:error, reason}
+ {:error, err} ->
+ {:error, err}
end
end
- @spec find_favicon_url(String.t(), String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
+ @spec find_favicon_url(String.t(), String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
defp find_favicon_url(url, body, headers) do
Logger.debug("finding favicon URL for #{url}")
@@ -85,20 +85,20 @@ defmodule Mobilizon.Service.RichMedia.Favicon do
end
end
- @spec find_favicon_in_root(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
+ @spec find_favicon_in_root(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
defp find_favicon_in_root(url, headers) do
uri = URI.parse(url)
favicon_url = "#{uri.scheme}://#{uri.host}/favicon.ico"
- case HTTPoison.head(favicon_url, headers, @options) do
- {:ok, %HTTPoison.Response{status_code: code}} when code in 200..299 ->
+ case Tesla.head(favicon_url, headers: headers, opts: @options) do
+ {:ok, %{status: code}} when code in 200..299 ->
{:ok, favicon_url}
- {:ok, %HTTPoison.Response{}} ->
+ {:ok, %{}} ->
{:error, "Error while doing a HEAD request on the favicon"}
- {:error, %HTTPoison.Error{reason: reason}} ->
- {:error, reason}
+ {:error, err} ->
+ {:error, err}
end
end
end
diff --git a/lib/service/rich_media/parser.ex b/lib/service/rich_media/parser.ex
index 05ba6429..fa16a328 100644
--- a/lib/service/rich_media/parser.ex
+++ b/lib/service/rich_media/parser.ex
@@ -12,7 +12,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
timeout: 10_000,
recv_timeout: 20_000,
follow_redirect: true,
- # TODO: Remove me once Hackney/HTTPoison fixes their shit with TLS1.3 and OTP 23
+ # TODO: Remove me once Hackney/HTTPoison fixes their issue with TLS1.3 and OTP 23
ssl: [{:versions, [:"tlsv1.2"]}]
]
@@ -46,7 +46,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
{:error, "Cachex error: #{inspect(e)}"}
end
- @spec parse_url(String.t(), List.t()) :: {:ok, map()} | {:error, any()}
+ @spec parse_url(String.t(), Enum.t()) :: {:ok, map()} | {:error, any()}
defp parse_url(url, options \\ []) do
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
headers = [{"User-Agent", user_agent}]
@@ -54,12 +54,12 @@ defmodule Mobilizon.Service.RichMedia.Parser do
try do
with {:ok, _} <- prevent_local_address(url),
- {:ok, %HTTPoison.Response{body: body, status_code: code, headers: response_headers}}
+ {:ok, %{body: body, status: code, headers: response_headers}}
when code in 200..299 <-
- HTTPoison.get(
+ Tesla.get(
url,
- headers,
- @options
+ headers: headers,
+ opts: @options
),
{:is_html, _response_headers, true} <-
{:is_html, response_headers, is_html(response_headers)} do
@@ -87,7 +87,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
end
end
- @spec get_data_for_media(List.t(), String.t()) :: map()
+ @spec get_data_for_media(Enum.t(), String.t()) :: map()
defp get_data_for_media(response_headers, url) do
data = %{title: get_filename_from_headers(response_headers) || get_filename_from_url(url)}
@@ -98,21 +98,21 @@ defmodule Mobilizon.Service.RichMedia.Parser do
end
end
- @spec is_html(List.t()) :: boolean
- defp is_html(headers) do
+ @spec is_html(Enum.t()) :: boolean
+ def is_html(headers) do
headers
|> get_header("Content-Type")
|> content_type_header_matches(["text/html", "application/xhtml"])
end
- @spec is_image(List.t()) :: boolean
+ @spec is_image(Enum.t()) :: boolean
defp is_image(headers) do
headers
|> get_header("Content-Type")
|> content_type_header_matches(["image/"])
end
- @spec content_type_header_matches(String.t() | nil, List.t()) :: boolean
+ @spec content_type_header_matches(String.t() | nil, Enum.t()) :: boolean
defp content_type_header_matches(header, content_types)
defp content_type_header_matches(nil, _content_types), do: false
@@ -120,15 +120,17 @@ defmodule Mobilizon.Service.RichMedia.Parser do
Enum.any?(content_types, fn content_type -> String.starts_with?(header, content_type) end)
end
- @spec get_header(List.t(), String.t()) :: String.t() | nil
+ @spec get_header(Enum.t(), String.t()) :: String.t() | nil
defp get_header(headers, key) do
+ key = String.downcase(key)
+
case List.keyfind(headers, key, 0) do
{^key, value} -> String.downcase(value)
nil -> nil
end
end
- @spec get_filename_from_headers(List.t()) :: String.t() | nil
+ @spec get_filename_from_headers(Enum.t()) :: String.t() | nil
defp get_filename_from_headers(headers) do
case get_header(headers, "Content-Disposition") do
nil -> nil
@@ -138,12 +140,16 @@ defmodule Mobilizon.Service.RichMedia.Parser do
@spec get_filename_from_url(String.t()) :: String.t()
defp get_filename_from_url(url) do
- %URI{path: path} = URI.parse(url)
+ case URI.parse(url) do
+ %URI{path: nil} ->
+ nil
- path
- |> String.split("/", trim: true)
- |> Enum.at(-1)
- |> URI.decode()
+ %URI{path: path} ->
+ path
+ |> String.split("/", trim: true)
+ |> Enum.at(-1)
+ |> URI.decode()
+ end
end
# The following is taken from https://github.com/elixir-plug/plug/blob/65986ad32f9aaae3be50dc80cbdd19b326578da7/lib/plug/parsers/multipart.ex#L207
diff --git a/lib/service/rich_media/parsers/oembed_parser.ex b/lib/service/rich_media/parsers/oembed_parser.ex
index 8f468f92..e1e3c19f 100644
--- a/lib/service/rich_media/parsers/oembed_parser.ex
+++ b/lib/service/rich_media/parsers/oembed_parser.ex
@@ -42,7 +42,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
end
defp get_oembed_data(url) do
- with {:ok, %HTTPoison.Response{body: json}} <- HTTPoison.get(url, [], @http_options),
+ with {:ok, %{body: json}} <- Tesla.get(url, opts: @http_options),
{:ok, data} <- Jason.decode(json),
data <- data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) do
{:ok, data}
diff --git a/lib/service/rich_media/parsers/ogp.ex b/lib/service/rich_media/parsers/ogp.ex
index 0b879aab..7bfcf88e 100644
--- a/lib/service/rich_media/parsers/ogp.ex
+++ b/lib/service/rich_media/parsers/ogp.ex
@@ -34,7 +34,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do
|> Map.put(:height, get_integer_value(data, :"image:height"))
end
- @spec get_integer_value(Map.t(), atom()) :: integer() | nil
+ @spec get_integer_value(map(), atom()) :: integer() | nil
defp get_integer_value(data, key) do
with value when not is_nil(value) <- Map.get(data, key),
{value, ""} <- Integer.parse(value) do
diff --git a/lib/service/statistics/statistics.ex b/lib/service/statistics/statistics.ex
index e7035cc8..105e3304 100644
--- a/lib/service/statistics/statistics.ex
+++ b/lib/service/statistics/statistics.ex
@@ -3,7 +3,7 @@ defmodule Mobilizon.Service.Statistics do
A module that provides cached statistics
"""
- alias Mobilizon.{Conversations, Events, Users}
+ alias Mobilizon.{Discussions, Events, Users}
def get_cached_value(key) do
case Cachex.fetch(:statistics, key, fn key ->
@@ -26,6 +26,6 @@ defmodule Mobilizon.Service.Statistics do
end
defp create_cache(:local_comments) do
- Conversations.count_local_comments()
+ Discussions.count_local_comments()
end
end
diff --git a/lib/service/workers/notification.ex b/lib/service/workers/notification.ex
index 3cb0346b..ec95fa98 100644
--- a/lib/service/workers/notification.ex
+++ b/lib/service/workers/notification.ex
@@ -98,7 +98,7 @@ defmodule Mobilizon.Service.Workers.Notification do
else
err ->
require Logger
- Logger.error(inspect(err))
+ Logger.debug(inspect(err))
end
end
diff --git a/lib/web/cache/activity_pub.ex b/lib/web/cache/activity_pub.ex
index 9475c30f..a6d55790 100644
--- a/lib/web/cache/activity_pub.ex
+++ b/lib/web/cache/activity_pub.ex
@@ -3,11 +3,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
ActivityPub related cache.
"""
- alias Mobilizon.{Actors, Conversations, Events, Resources, Todos, Tombstone}
+ alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone}
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay
+ alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Web.Endpoint
@@ -61,7 +62,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
{:commit, Comment.t()} | {:ignore, nil}
def get_comment_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid ->
- case Conversations.get_comment_from_uuid_with_preload(uuid) do
+ case Discussions.get_comment_from_uuid_with_preload(uuid) do
%Comment{} = comment ->
{:commit, comment}
@@ -88,6 +89,40 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
end)
end
+ @doc """
+ Gets a post by its slug, with all associations loaded.
+ """
+ @spec get_post_by_slug_with_preload(String.t()) ::
+ {:commit, Post.t()} | {:ignore, nil}
+ def get_post_by_slug_with_preload(slug) do
+ Cachex.fetch(@cache, "post_" <> slug, fn "post_" <> slug ->
+ case Posts.get_post_by_slug_with_preloads(slug) do
+ %Post{} = post ->
+ {:commit, post}
+
+ nil ->
+ {:ignore, nil}
+ end
+ end)
+ end
+
+ @doc """
+ Gets a discussion by its slug, with all associations loaded.
+ """
+ @spec get_discussion_by_slug_with_preload(String.t()) ::
+ {:commit, Discussion.t()} | {:ignore, nil}
+ def get_discussion_by_slug_with_preload(slug) do
+ Cachex.fetch(@cache, "discussion_" <> slug, fn "discussion_" <> slug ->
+ case Discussions.get_discussion_by_slug(slug) do
+ %Discussion{} = discussion ->
+ {:commit, discussion}
+
+ nil ->
+ {:ignore, nil}
+ end
+ end)
+ end
+
@doc """
Gets a todo list by its UUID, with all associations loaded.
"""
diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex
index eb60c7e8..31542c38 100644
--- a/lib/web/cache/cache.ex
+++ b/lib/web/cache/cache.ex
@@ -23,5 +23,7 @@ defmodule Mobilizon.Web.Cache do
defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub
+ defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub
+ defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub
defdelegate get_relay, to: ActivityPub
end
diff --git a/lib/web/controllers/activity_pub_controller.ex b/lib/web/controllers/activity_pub_controller.ex
index 99cd5bd8..e4739a4d 100644
--- a/lib/web/controllers/activity_pub_controller.ex
+++ b/lib/web/controllers/activity_pub_controller.ex
@@ -14,6 +14,7 @@ defmodule Mobilizon.Web.ActivityPubController do
alias Mobilizon.Web.ActivityPub.ActorView
alias Mobilizon.Web.Cache
+ alias Plug.Conn
require Logger
@@ -33,96 +34,40 @@ defmodule Mobilizon.Web.ActivityPubController do
end
end
- def following(conn, %{"name" => name, "page" => page}) do
- with {page, ""} <- Integer.parse(page),
- %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("following.json", %{actor: actor, page: page}))
- end
+ def following(conn, args) do
+ actor_collection(conn, "following", args)
end
- def following(conn, %{"name" => name}) do
- with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("following.json", %{actor: actor}))
- end
+ def followers(conn, args) do
+ actor_collection(conn, "followers", args)
end
- def followers(conn, %{"name" => name, "page" => page}) do
- with {page, ""} <- Integer.parse(page),
- %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("followers.json", %{actor: actor, page: page}))
- end
+ def members(conn, args) do
+ actor_collection(conn, "members", args)
end
- def followers(conn, %{"name" => name}) do
- with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("followers.json", %{actor: actor}))
- end
+ def resources(conn, args) do
+ actor_collection(conn, "resources", args)
end
- def members(conn, %{"name" => name, "page" => page}) do
- with {page, ""} <- Integer.parse(page),
- %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(
- ActorView.render("members.json", %{
- group: group,
- page: page,
- actor_applicant: Map.get(conn.assigns, :actor)
- })
- )
- end
+ def posts(conn, args) do
+ actor_collection(conn, "posts", args)
end
- def members(conn, %{"name" => name}) do
- with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(
- ActorView.render("members.json", %{
- group: group,
- actor_applicant: Map.get(conn.assigns, :actor)
- })
- )
- end
+ def todos(conn, args) do
+ actor_collection(conn, "todos", args)
end
- def resources(conn, %{"name" => name}) do
- with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(
- ActorView.render("resources.json", %{
- group: group,
- actor_applicant: Map.get(conn.assigns, :actor)
- })
- )
- end
+ def events(conn, args) do
+ actor_collection(conn, "events", args)
end
- def outbox(conn, %{"name" => name, "page" => page}) do
- with {page, ""} <- Integer.parse(page),
- %Actor{} = actor <- Actors.get_local_actor_by_name(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("outbox.json", %{actor: actor, page: page}))
- end
+ def discussions(conn, args) do
+ actor_collection(conn, "discussions", args)
end
- def outbox(conn, %{"name" => name}) do
- with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do
- conn
- |> put_resp_header("content-type", "application/activity+json")
- |> json(ActorView.render("outbox.json", %{actor: actor}))
- end
+ def outbox(conn, args) do
+ actor_collection(conn, "outbox", args)
end
# TODO: Ensure that this inbox is a recipient of the message
@@ -178,4 +123,34 @@ defmodule Mobilizon.Web.ActivityPubController do
|> put_status(500)
|> json("Unknown Error")
end
+
+ @spec actor_collection(Conn.t(), String.t(), map()) :: Conn.t()
+
+ defp actor_collection(conn, collection, %{"name" => name, "page" => page}) do
+ with {page, ""} <- Integer.parse(page),
+ %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(
+ ActorView.render("#{collection}.json", %{
+ actor: actor,
+ page: page,
+ actor_applicant: Map.get(conn.assigns, :actor)
+ })
+ )
+ end
+ end
+
+ defp actor_collection(conn, collection, %{"name" => name}) do
+ with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(
+ ActorView.render("#{collection}.json", %{
+ actor: actor,
+ actor_applicant: Map.get(conn.assigns, :actor)
+ })
+ )
+ end
+ end
end
diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex
index 18b65b18..c5766376 100644
--- a/lib/web/controllers/page_controller.ex
+++ b/lib/web/controllers/page_controller.ex
@@ -4,7 +4,7 @@ defmodule Mobilizon.Web.PageController do
"""
use Mobilizon.Web, :controller
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Tombstone
@@ -40,14 +40,36 @@ defmodule Mobilizon.Web.PageController do
render_or_error(conn, &checks?/3, status, :resource, resource)
end
- def resources(conn, %{"name" => _name}) do
- case get_format(conn) do
- "html" ->
- render(conn, :index)
+ @spec post(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
+ def post(conn, %{"slug" => slug}) do
+ {status, post} = Cache.get_post_by_slug_with_preload(slug)
+ render_or_error(conn, &checks?/3, status, :post, post)
+ end
- "activity-json" ->
- ActivityPubController.call(conn, :resources)
- end
+ @spec discussion(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
+ def discussion(conn, %{"slug" => slug}) do
+ {status, discussion} = Cache.get_discussion_by_slug_with_preload(slug)
+ render_or_error(conn, &checks?/3, status, :discussion, discussion)
+ end
+
+ def resources(conn, %{"name" => _name}) do
+ handle_collection_route(conn, :resources)
+ end
+
+ def posts(conn, %{"name" => _name}) do
+ handle_collection_route(conn, :posts)
+ end
+
+ def discussions(conn, %{"name" => _name}) do
+ handle_collection_route(conn, :discussions)
+ end
+
+ def events(conn, %{"name" => _name}) do
+ handle_collection_route(conn, :events)
+ end
+
+ def todos(conn, %{"name" => _name}) do
+ handle_collection_route(conn, :todos)
end
@spec todo_list(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
@@ -71,6 +93,16 @@ defmodule Mobilizon.Web.PageController do
end
end
+ defp handle_collection_route(conn, collection) do
+ case get_format(conn) do
+ "html" ->
+ render(conn, :index)
+
+ "activity-json" ->
+ ActivityPubController.call(conn, collection)
+ end
+ end
+
defp render_or_error(conn, check_fn, status, object_type, object) do
case check_fn.(conn, status, object) do
true ->
diff --git a/lib/web/email/event.ex b/lib/web/email/event.ex
index a56a8c17..32336bf6 100644
--- a/lib/web/email/event.ex
+++ b/lib/web/email/event.ex
@@ -82,12 +82,4 @@ defmodule Mobilizon.Web.Email.Event do
|> Email.Event.event_updated(actor, old_event, event, diff, locale)
|> Email.Mailer.deliver_later()
end
-
- defp send_notification_for_event_update_to_participant(user, old_event, new_event, diff) do
- require Logger
- Logger.error(inspect(user))
- Logger.error(inspect(old_event))
- Logger.error(inspect(new_event))
- Logger.error(inspect(diff))
- end
end
diff --git a/lib/web/proxy/reverse_proxy.ex b/lib/web/proxy/reverse_proxy.ex
index 045e8ae2..ea1a5756 100644
--- a/lib/web/proxy/reverse_proxy.ex
+++ b/lib/web/proxy/reverse_proxy.ex
@@ -84,7 +84,6 @@ defmodule Mobilizon.Web.ReverseProxy do
| {:redirect_on_failure, boolean}
@hackney Application.get_env(:mobilizon, :hackney, :hackney)
- @httpoison Application.get_env(:mobilizon, :httpoison, HTTPoison)
@default_hackney_options []
@@ -108,7 +107,6 @@ defmodule Mobilizon.Web.ReverseProxy do
hackney_opts =
@default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, []))
- |> @httpoison.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts)
diff --git a/lib/web/router.ex b/lib/web/router.ex
index 725afa2d..15ec71b3 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -87,17 +87,23 @@ defmodule Mobilizon.Web.Router do
get("/resource/:uuid", PageController, :resource, as: "resource")
get("/todo-list/:uuid", PageController, :todo_list, as: "todo_list")
get("/todo/:uuid", PageController, :todo, as: "todo")
+ get("/@:name/todos", PageController, :todos)
get("/@:name/resources", PageController, :resources)
+ get("/@:name/posts", PageController, :posts)
+ get("/@:name/discussions", PageController, :discussions)
+ get("/@:name/events", PageController, :events)
+ get("/p/:slug", PageController, :post)
+ get("/@:name/c/:slug", PageController, :discussion)
end
scope "/", Mobilizon.Web do
pipe_through(:activity_pub)
+ pipe_through(:activity_pub_signature)
get("/@:name/outbox", ActivityPubController, :outbox)
get("/@:name/following", ActivityPubController, :following)
get("/@:name/followers", ActivityPubController, :followers)
get("/@:name/members", ActivityPubController, :members)
- get("/@:name/todo-lists", ActivityPubController, :todo_lists)
end
scope "/", Mobilizon.Web do
diff --git a/lib/web/upload/upload.ex b/lib/web/upload/upload.ex
index dd6ee065..0435336b 100644
--- a/lib/web/upload/upload.ex
+++ b/lib/web/upload/upload.ex
@@ -64,7 +64,7 @@ defmodule Mobilizon.Web.Upload do
}
defstruct [:id, :name, :tempfile, :content_type, :path, :size]
- @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
+ @spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()}
def store(upload, opts \\ []) do
opts = get_opts(opts)
diff --git a/lib/web/upload/uploader/uploader.ex b/lib/web/upload/uploader/uploader.ex
index fce8fdf6..57c9de99 100644
--- a/lib/web/upload/uploader/uploader.ex
+++ b/lib/web/upload/uploader/uploader.ex
@@ -37,7 +37,7 @@ defmodule Mobilizon.Web.Upload.Uploader do
@callback remove_file(file_spec()) :: :ok | {:ok, file_spec()} | {:error, String.t()}
- @callback http_callback(Plug.Conn.t(), Map.t()) ::
+ @callback http_callback(Plug.Conn.t(), map()) ::
{:ok, Plug.Conn.t()}
| {:ok, Plug.Conn.t(), file_spec()}
| {:error, Plug.Conn.t(), String.t()}
diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex
index a1c33d9e..3599cf39 100644
--- a/lib/web/views/activity_pub/actor_view.ex
+++ b/lib/web/views/activity_pub/actor_view.ex
@@ -1,16 +1,21 @@
defmodule Mobilizon.Web.ActivityPub.ActorView do
use Mobilizon.Web, :view
- alias Mobilizon.Actors
+ alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos}
alias Mobilizon.Actors.{Actor, Member}
- alias Mobilizon.Resources
- alias Mobilizon.Resources.Resource
-
+ alias Mobilizon.Discussions.Discussion
+ alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Posts.Post
+ alias Mobilizon.Resources.Resource
+ alias Mobilizon.Storage.Page
+ alias Mobilizon.Todos.TodoList
@private_visibility_empty_collection %{elements: [], total: 0}
+ @json_ld_header Utils.make_json_ld_header()
+ @selected_member_roles ~w(creator administrator moderator member)a
def render("actor.json", %{actor: actor}) do
actor
@@ -18,145 +23,120 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
|> Map.merge(Utils.make_json_ld_header())
end
- def render("following.json", %{actor: actor, page: page}) do
- %{total: total, elements: following} =
- if Actor.is_public_visibility(actor),
- do: Actors.build_followings_for_actor(actor, page),
- else: @private_visibility_empty_collection
+ @doc """
+ Render an actor collection
+ """
+ @spec render(String.t(), map()) :: map()
+ def render(view_name, %{actor: %Actor{} = actor} = args) do
+ is_root? = is_nil(Map.get(args, :page))
+ page = Map.get(args, :page, 1)
+ collection_name = String.trim_trailing(view_name, ".json")
+ collection_name = String.to_existing_atom(collection_name)
- following
- |> collection(actor.preferred_username, :following, page, total)
- |> Map.merge(Utils.make_json_ld_header())
+ %{total: total, elements: elements} =
+ if can_get_collection?(collection_name, actor, Map.get(args, :actor_applicant)),
+ do: fetch_collection(collection_name, actor, page),
+ else: default_collection(collection_name, actor, page)
+
+ collection =
+ if is_root? do
+ root_collection(elements, actor, collection_name, total)
+ else
+ collection(elements, actor.preferred_username, collection_name, page, total)
+ end
+
+ Map.merge(collection, @json_ld_header)
end
- def render("following.json", %{actor: actor}) do
- %{total: total, elements: following} =
- if Actor.is_public_visibility(actor),
- do: Actors.build_followings_for_actor(actor),
- else: @private_visibility_empty_collection
-
+ @spec root_collection(Enum.t(), Actor.t(), atom(), integer()) :: map()
+ defp root_collection(
+ elements,
+ %Actor{preferred_username: preferred_username, url: actor_url},
+ collection,
+ total
+ ) do
%{
- "id" => Actor.build_url(actor.preferred_username, :following),
+ "id" => Actor.build_url(preferred_username, collection),
+ "attributedTo" => actor_url,
"type" => "OrderedCollection",
"totalItems" => total,
- "first" => collection(following, actor.preferred_username, :following, 1, total)
+ "first" => collection(elements, preferred_username, collection, 1, total)
}
- |> Map.merge(Utils.make_json_ld_header())
end
- def render("followers.json", %{actor: actor, page: page}) do
- %{total: total, elements: followers} =
- if Actor.is_public_visibility(actor),
- do: Actors.build_followers_for_actor(actor, page),
- else: @private_visibility_empty_collection
-
- followers
- |> collection(actor.preferred_username, :followers, page, total)
- |> Map.merge(Utils.make_json_ld_header())
+ @spec fetch_collection(atom(), Actor.t(), integer()) :: Page.t()
+ defp fetch_collection(:following, actor, page) do
+ Actors.build_followings_for_actor(actor, page)
end
- def render("followers.json", %{actor: actor}) do
- %{total: total, elements: followers} =
- if Actor.is_public_visibility(actor),
- do: Actors.build_followers_for_actor(actor),
- else: @private_visibility_empty_collection
-
- %{
- "id" => actor.followers_url,
- "type" => "OrderedCollection",
- "totalItems" => total,
- "first" => collection(followers, actor.preferred_username, :followers, 1, total)
- }
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:followers, actor, page) do
+ Actors.build_followers_for_actor(actor, page)
end
- def render("members.json", %{group: group, page: page, actor_applicant: actor_applicant}) do
- %{total: total, elements: members} =
- if Actor.is_public_visibility(group) ||
- actor_applicant_group_member?(group, actor_applicant),
- do: Actors.list_members_for_group(group, page),
- else: @private_visibility_empty_collection
-
- members
- |> collection(group.preferred_username, :members, page, total)
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:members, actor, page) do
+ Actors.list_members_for_group(actor, @selected_member_roles, page)
end
- def render("members.json", %{group: group, actor_applicant: actor_applicant}) do
- %{total: total, elements: members} =
- if Actor.is_public_visibility(group) ||
- actor_applicant_group_member?(group, actor_applicant),
- do: Actors.list_members_for_group(group),
- else: @private_visibility_empty_collection
-
- %{
- "id" => group.url,
- "attributedTo" => group.url,
- "type" => "OrderedCollection",
- "totalItems" => total,
- "first" => collection(members, group.preferred_username, :members, 1, total)
- }
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:resources, actor, page) do
+ Resources.get_resources_for_group(actor, page)
end
- def render("resources.json", %{group: group, page: page, actor_applicant: actor_applicant}) do
- %{total: total, elements: resources} =
- if Actor.is_public_visibility(group) ||
- actor_applicant_group_member?(group, actor_applicant),
- do: Resources.get_top_level_resources_for_group(group),
- else: @private_visibility_empty_collection
-
- resources
- |> collection(group.preferred_username, :resources, page, total)
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:discussions, actor, page) do
+ Discussions.find_discussions_for_actor(actor.id, page)
end
- def render("resources.json", %{group: group, actor_applicant: actor_applicant}) do
- %{total: total, elements: resources} =
- if Actor.is_public_visibility(group) ||
- actor_applicant_group_member?(group, actor_applicant),
- do: Resources.get_top_level_resources_for_group(group),
- else: @private_visibility_empty_collection
-
- %{
- "id" => group.resources_url,
- "attributedTo" => group.url,
- "type" => "OrderedCollection",
- "totalItems" => total,
- "first" => collection(resources, group.preferred_username, :resources, 1, total)
- }
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:posts, actor, page) do
+ Posts.get_posts_for_group(actor, page)
end
- def render("outbox.json", %{actor: actor, page: page}) do
- %{total: total, elements: followers} =
- if Actor.is_public_visibility(actor),
- do: ActivityPub.fetch_public_activities_for_actor(actor, page),
- else: @private_visibility_empty_collection
-
- followers
- |> collection(actor.preferred_username, :outbox, page, total)
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:events, actor, page) do
+ Events.list_organized_events_for_group(actor, page)
end
- def render("outbox.json", %{actor: actor}) do
- %{total: total, elements: followers} =
- if Actor.is_public_visibility(actor),
- do: ActivityPub.fetch_public_activities_for_actor(actor),
- else: @private_visibility_empty_collection
-
- %{
- "id" => Actor.build_url(actor.preferred_username, :outbox),
- "type" => "OrderedCollection",
- "totalItems" => total,
- "first" => collection(followers, actor.preferred_username, :outbox, 1, total)
- }
- |> Map.merge(Utils.make_json_ld_header())
+ defp fetch_collection(:todos, actor, page) do
+ Todos.get_todo_lists_for_group(actor, page)
end
+ @spec fetch_collection(atom(), Actor.t(), integer()) :: %{total: integer(), elements: Enum.t()}
+ defp fetch_collection(:outbox, actor, page) do
+ ActivityPub.fetch_public_activities_for_actor(actor, page)
+ end
+
+ defp fetch_collection(_, _, _), do: @private_visibility_empty_collection
+
+ @spec can_get_collection?(atom(), Actor.t(), Actor.t()) :: boolean()
+ # Outbox only contains public activities
+ defp can_get_collection?(collection, %Actor{visibility: visibility} = _actor, _actor_applicant)
+ when visibility in [:public, :unlisted] and collection in [:outbox, :followers, :following],
+ do: true
+
+ defp can_get_collection?(_collection_name, %Actor{} = actor, %Actor{} = actor_applicant),
+ do: actor_applicant_group_member?(actor, actor_applicant)
+
+ defp can_get_collection?(_, _, _), do: false
+
+ # Posts and events allows to browse public content
+ defp default_collection(:posts, %Actor{} = actor, page),
+ do: Posts.get_public_posts_for_group(actor, page)
+
+ defp default_collection(:events, %Actor{} = actor, page),
+ do: Events.list_public_events_for_actor(actor, page)
+
+ defp default_collection(_, _, _), do: @private_visibility_empty_collection
+
@spec collection(list(), String.t(), atom(), integer(), integer()) :: map()
defp collection(collection, preferred_username, endpoint, page, total)
- when endpoint in [:followers, :following, :outbox, :members, :resources, :todos] do
+ when endpoint in [
+ :followers,
+ :following,
+ :outbox,
+ :members,
+ :resources,
+ :todos,
+ :posts,
+ :events,
+ :discussions
+ ] do
offset = (page - 1) * 10
map = %{
@@ -178,6 +158,10 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
def item(%Actor{url: url}), do: url
def item(%Member{} = member), do: Convertible.model_to_as(member)
def item(%Resource{} = resource), do: Convertible.model_to_as(resource)
+ def item(%Discussion{} = discussion), do: Convertible.model_to_as(discussion)
+ def item(%Post{} = post), do: Convertible.model_to_as(post)
+ def item(%Event{} = event), do: Convertible.model_to_as(event)
+ def item(%TodoList{} = todo_list), do: Convertible.model_to_as(todo_list)
defp actor_applicant_group_member?(%Actor{}, nil), do: false
diff --git a/lib/web/views/json_ld/object_view.ex b/lib/web/views/json_ld/object_view.ex
index 55aad9fe..03fa0fbb 100644
--- a/lib/web/views/json_ld/object_view.ex
+++ b/lib/web/views/json_ld/object_view.ex
@@ -4,24 +4,30 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event
-
+ alias Mobilizon.Posts.Post
alias Mobilizon.Web.JsonLD.ObjectView
alias Mobilizon.Web.MediaProxy
def render("event.json", %{event: %Event{} = event}) do
- # TODO: event.description is actually markdown!
+ organizer = %{
+ "@type" => if(event.organizer_actor.type == :Group, do: "Organization", else: "Person"),
+ "name" => Actor.display_name(event.organizer_actor)
+ }
json_ld = %{
"@context" => "https://schema.org",
"@type" => "Event",
"name" => event.title,
"description" => event.description,
- "performer" => %{
- "@type" =>
- if(event.organizer_actor.type == :Group, do: "PerformingGroup", else: "Person"),
- "name" => Actor.display_name(event.organizer_actor)
- },
- "location" => render_one(event.physical_address, ObjectView, "place.json", as: :address)
+ # We assume for now performer == organizer
+ "performer" => organizer,
+ "organizer" => organizer,
+ "location" => render_one(event.physical_address, ObjectView, "place.json", as: :address),
+ "eventStatus" =>
+ if(event.status == :cancelled,
+ do: "https://schema.org/EventCancelled",
+ else: "https://schema.org/EventScheduled"
+ )
}
json_ld =
@@ -62,4 +68,18 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
end
def render("place.json", nil), do: %{}
+
+ def render("post.json", %{post: %Post{} = post}) do
+ %{
+ "@context" => "https://schema.org",
+ "@type" => "Article",
+ "name" => post.title,
+ "author" => %{
+ "@type" => "Organization",
+ "name" => Actor.display_name(post.attributed_to)
+ },
+ "datePublished" => post.publish_at,
+ "dateModified" => post.updated_at
+ }
+ end
end
diff --git a/lib/web/views/page_view.ex b/lib/web/views/page_view.ex
index 3615ddc1..b70a4ff4 100644
--- a/lib/web/views/page_view.ex
+++ b/lib/web/views/page_view.ex
@@ -6,7 +6,7 @@ defmodule Mobilizon.Web.PageView do
use Mobilizon.Web, :view
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event
alias Mobilizon.Resources.Resource
alias Mobilizon.Tombstone
@@ -42,6 +42,12 @@ defmodule Mobilizon.Web.PageView do
|> Map.merge(Utils.make_json_ld_header())
end
+ def render("discussion.activity-json", %{conn: %{assigns: %{object: %Discussion{} = resource}}}) do
+ resource
+ |> Convertible.model_to_as()
+ |> Map.merge(Utils.make_json_ld_header())
+ end
+
def render("resource.activity-json", %{conn: %{assigns: %{object: %Resource{} = resource}}}) do
resource
|> Convertible.model_to_as()
@@ -49,12 +55,15 @@ defmodule Mobilizon.Web.PageView do
end
def render(page, %{object: object, conn: conn} = _assigns)
- when page in ["actor.html", "event.html", "comment.html"] do
+ when page in ["actor.html", "event.html", "comment.html", "post.html"] do
locale = get_locale(conn)
tags = object |> Metadata.build_tags(locale)
inject_tags(tags, locale)
end
+ # Discussions are private, no need to embed metadata
+ def render("discussion.html", params), do: render("index.html", params)
+
def render("index.html", %{conn: conn}) do
tags = Instance.build_tags()
inject_tags(tags, get_locale(conn))
diff --git a/lib/web/views/utils.ex b/lib/web/views/utils.ex
index 234e222c..5fd5417d 100644
--- a/lib/web/views/utils.ex
+++ b/lib/web/views/utils.ex
@@ -5,7 +5,7 @@ defmodule Mobilizon.Web.Views.Utils do
alias Mobilizon.Service.Metadata.Utils, as: MetadataUtils
- @spec inject_tags(List.t(), String.t()) :: {:safe, String.t()}
+ @spec inject_tags(Enum.t(), String.t()) :: {:safe, String.t()}
def inject_tags(tags, locale \\ "en") do
with {:ok, index_content} <- File.read(index_file_path()) do
do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale)
diff --git a/mix.exs b/mix.exs
index 1fb048c1..6434273a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -87,7 +87,6 @@ defmodule Mobilizon.Mixfile do
{:timex, "~> 3.0"},
{:icalendar, github: "tcitworld/icalendar"},
{:exgravatar, "~> 2.0.1"},
- {:httpoison, "~> 1.0"},
# {:json_ld, "~> 0.3"},
{:jason, "~> 1.2"},
{:ex_crypto, "~> 0.10.0"},
@@ -118,7 +117,7 @@ defmodule Mobilizon.Mixfile do
{:ex_optimizer, "~> 0.1"},
{:progress_bar, "~> 2.0"},
{:oban, "~> 1.2.0"},
- {:floki, "~> 0.26.0"},
+ {:floki, "~> 0.27.0"},
{:ip_reserved, "~> 0.1.0"},
{:fast_sanitize, "~> 0.1"},
{:ueberauth, "~> 0.6"},
@@ -131,6 +130,8 @@ defmodule Mobilizon.Mixfile do
git: "https://github.com/tcitworld/ueberauth_keycloak.git", branch: "upgrade-deps"},
{:ueberauth_gitlab_strategy,
git: "https://github.com/tcitworld/ueberauth_gitlab.git", branch: "upgrade-deps"},
+ {:ecto_shortuuid, "~> 0.1"},
+ {:tesla, "~> 1.3.0"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]},
@@ -142,7 +143,8 @@ defmodule Mobilizon.Mixfile do
{:exvcr, "~> 0.10", only: :test},
{:credo, "~> 1.4.0", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.4", only: :test},
- {:elixir_feed_parser, "~> 2.1.0", only: :test}
+ {:elixir_feed_parser, "~> 2.1.0", only: :test},
+ {:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
end
@@ -335,7 +337,7 @@ defmodule Mobilizon.Mixfile do
Mobilizon.GraphQL.Schema.Actors.PersonType,
Mobilizon.GraphQL.Schema.AddressType,
Mobilizon.GraphQL.Schema.AdminType,
- Mobilizon.GraphQL.Schema.Conversations.CommentType,
+ Mobilizon.GraphQL.Schema.Discussions.CommentType,
Mobilizon.GraphQL.Schema.ConfigType,
Mobilizon.GraphQL.Schema.EventType,
Mobilizon.GraphQL.Schema.Events.FeedTokenType,
diff --git a/mix.lock b/mix.lock
index 9445c84a..e4521e3e 100644
--- a/mix.lock
+++ b/mix.lock
@@ -23,12 +23,13 @@
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
- "earmark": {:hex, :earmark, "1.4.9", "837e4c1c5302b3135e9955f2bbf52c6c52e950c383983942b68b03909356c0d9", [:mix], [{:earmark_parser, ">= 1.4.9", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "0d72df7d13a3dc8422882bed5263fdec5a773f56f7baeb02379361cb9e5b0d8e"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.9", "819bda2049e6ee1365424e4ced1ba65806eacf0d2867415f19f3f80047f8037b", [:mix], [], "hexpm", "8bf54fddabf2d7e137a0c22660e71b49d5a0a82d1fb05b5af62f2761cd6485c4"},
+ "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "2.0.1", "2177c1c253f6dd3efd4b56d1cb76104d0a6ef044c6b9a7a0ad6d32665c4111e5", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "a3cc73211f2e75b89a03332183812ebe1ac08be2e25a1df5aa3d1422f92c45c3"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
- "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
+ "ecto_shortuuid": {:hex, :ecto_shortuuid, "0.1.3", "d36aede64edf256e4b769be2ad15a8ad5d9d1ff8ad46befe39e8cb4489abcd05", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:shortuuid, "~> 2.1.1", [hex: :shortuuid, repo: "hexpm", optional: false]}], "hexpm", "d215c8ced7125265de94d55abc696125942caef33439cf281fafded9744a4294"},
+ "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
"elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
@@ -36,25 +37,25 @@
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
"ex_cldr": {:hex, :ex_cldr, "2.16.1", "905b03c38b5fb51668a347f2e6b586bcb2c0816cd98f7d913104872c43cbc61f", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.9", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "006e500769982e57e6f3e32cbc4664345f78b014bb5ff48ddc394d67c86c1a8d"},
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.9.0", "ace1c57ba3850753c9ac6ddb89dc0c9a9e5e1c57ecad587e21c8925ad30a3838", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a4b07773e2a326474f44a6bc51fffbec634859a1bad5cc6e6eb55eba45115541"},
- "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.5.0", "e369ae3c1cd5cd20aa20988b153fd2902b4ab08aec63ca8757d7104bdb79f867", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ba16b1df60bcec52c986481bbdfa7cfaec899b610f869d2b3c5a9a8149f67668"},
+ "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.6.1", "accc6b409c88c3ce4d9439f0a92e375470b788efa95d8fa5a054aa9e8950e972", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca9817eadc78f11f1084f64c579288513b0042eaa39ea2337d68ba959ebae1ad"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.1", "9439d1c40cfd03c3d8f3f60f5d3e3f2c6eaf0fd714541d687531cce78cfb9909", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "62a2f8d41ec6e789137bbf3ac7c944885a8ef6b7ce475905d056d1805b482427"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.1", "dced7ffee69c4830593258b69b294adb4c65cf539e1d8ae0a4de31cfc8aa56a0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c6a4b69ef80b8ffbb6c8fb69a2b365ba542580e0f76a15d8c6ee9142bd1b97ea"},
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
- "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
+ "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"},
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"},
"ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"},
"ex_optimizer": {:hex, :ex_optimizer, "0.1.0", "1d12f7ea289092a38a794b84bd2f42c1e0621cb307c0f3e6a7df620839af2937", [:mix], [{:file_info, "~> 0.0.4", [hex: :file_info, repo: "hexpm", optional: false]}], "hexpm", "a409cb91472e08d4791a129effe4687982f85e2debcb4ccb1a3711a36bfdc428"},
"ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm", "fddf5054dd5fd2f809e837b749570baa5c9798e11d0163921baec49b7d5762f2"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"},
- "excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"},
+ "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"},
"exgravatar": {:hex, :exgravatar, "2.0.2", "638412896170409da114f98947d3f8d4f38e851b0e329c1cc4cd324d5e2ea081", [:mix], [], "hexpm", "f3deb5baa6fcf354a965d794ee73a956d95f1f79f41bddf69800c713cfb014a1"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"},
- "exvcr": {:hex, :exvcr, "0.11.1", "a5e5f57a67538e032e16cfea6cfb1232314fb146e3ceedf1cde4a11f12fb7a58", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "984a4d52d9e01d5f0e28d45718565a41dffab3ac18e029ae45d42f16a2a58a1d"},
+ "exvcr": {:hex, :exvcr, "0.11.2", "24aec6ad13a659f10591911089c01f8d2691e2fff75710c924b64437cc1b36a1", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "0dad8a3065af4040933bc3ec296f28654b04e993a81054199c832fa86329e80f"},
"fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
"fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
"file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
- "floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"},
+ "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"geo": {:hex, :geo, "3.3.3", "1119302b20d21515fbcec0a180b82653524067873ed333e7fa1f55e39959d702", [:mix], [], "hexpm", "8297ae0ac5ce47bb608b2bc8a63030460020ae537de9464a7a652f25baf6d2c1"},
"geo_postgis": {:hex, :geo_postgis, "3.3.1", "45bc96b9121d0647341685dc9d44956d61338707482d655c803500676b0413a1", [:mix], [{:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "3c3957d8750e3effd565f068ee658ef0e881f9a07084a23f6c5ef8262d09b8e9"},
@@ -67,6 +68,8 @@
"guardian_db": {:hex, :guardian_db, "2.0.3", "18c847efbf7ec3c0dd44c7aecaeeb2777588bbb8d2073ffc36e71037108b3be6", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "17306e09498bca379fb8eded2ac44d7690f738ca14b17080d06a948d034ea087"},
"guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "21f439246715192b231f228680465d1ed5fbdf01555a4a3b17165532f5f9a08c"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
+ "hammox": {:hex, :hammox, "0.2.5", "55436c392c242ae893ebddda8ad20bafb3a5fd6d9899dd44dbf29b84420cf316", [:mix], [{:mox, "~> 0.5", [hex: :mox, repo: "hexpm", optional: false]}, {:ordinal, "~> 0.1", [hex: :ordinal, repo: "hexpm", optional: false]}], "hexpm", "c4862a86eeec8531f14795b584677870c58a4c8de5eab5730904db3a27b836f2"},
+ "hashids": {:hex, :hashids, "2.0.4", "ea47a2c2018b7ffb4f5ac9b0f8ea0af6d6159b9e190c5ed09f0ea83276968e0f", [:mix], [], "hexpm", "812e2c7ae763609a47acdd4c64d58c72f63bcfd741e6c605127e43af1507e019"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.0", "0310d27d7bafb662f30bff22ec732a72414799c83eaf44239781fd23b96216c0", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c5d79626be0b6e50c19ecdfb783ee26e85bd3a77436b488379ce6dc104ec4593"},
"http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2d4b1c2579d85534035f12c9e1260abdf6d03a9ad4f515b2ee53b50e68c8b787"},
@@ -92,13 +95,15 @@
"mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"},
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"},
+ "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
"oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
+ "ordinal": {:hex, :ordinal, "0.1.0", "2f7a1a64ff4be44b8a674718bb00d1584188fe92fa2fa48b95b1e72096d74a34", [:mix], [], "hexpm", "9f3d0a50c285ac99faa9626376e11afa6fc83d42e95166768b37d176cff485a3"},
"paddle": {:hex, :paddle, "0.1.4", "3697996d79e3d771d6f7560a23e4bad1ed7b7f7fd3e784f97bc39565963b2b13", [:mix], [], "hexpm", "fc719a9e7c86f319b9f4bf413d6f0f326b0c4930d5bc6630d074598ed38e2143"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
- "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
+ "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"},
@@ -112,14 +117,16 @@
"progress_bar": {:hex, :progress_bar, "2.0.0", "447285f533b4b8717881fdb7160c7360c2f2ab57276f8904ce6d40482857e573", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "9d8b879f322fd5563e8e7ec39f1d02a9da3ffc36019f05287788744e88260fde"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"rsa_ex": {:hex, :rsa_ex, "0.4.0", "e28dd7dc5236e156df434af0e4aa822384c8866c928e17b785d4edb7c253b558", [:mix], [], "hexpm", "40e1f08e8401da4be59a6dd0f4da30c42d5bb01703161f0208d839d97db27f4e"},
+ "shortuuid": {:hex, :shortuuid, "2.1.2", "14dbafdb2f6c7213fdfcc05c7572384b5051a7b1621170018ad4c05504bd96c1", [:mix], [], "hexpm", "d9b0c4f37500ea5199b6275ece872e213e9f45a015caf4aa777cec84f63ad353"},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
+ "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
- "ueberauth_discord": {:hex, :ueberauth_discord, "0.5.0", "52421277b93fda769b51636e542b5085f3861efdc7fa48ac4bedb6dae0b645e1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "9a3808baf44297e26bd5042ba9ea5398aa60023e054eb9a5ac8a4eacd0467a78"},
+ "ueberauth_discord": {:hex, :ueberauth_discord, "0.5.1", "3403cd2da533487370698152497b6164e5e1058e29241e78cd34ce75e727a168", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "623470f46553eb902f3cc2c922c33964300152402357daa44244df432b4e4f04"},
"ueberauth_facebook": {:hex, :ueberauth_facebook, "0.8.1", "c254be4ab367c276773c2e41d3c0fe343ae118e244afc8d5a4e3e5c438951fdc", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "c2cf210ef45bd20611234ef17517f9d1dff6b31d3fb6ad96789143eb0943f540"},
"ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"},
"ueberauth_gitlab_strategy": {:git, "https://github.com/tcitworld/ueberauth_gitlab.git", "9fc5d30b5d87ff7cdef293a1c128f25777dcbe59", [branch: "upgrade-deps"]},
diff --git a/priv/repo/migrations/20190103150805_fix_event_visibility.exs b/priv/repo/migrations/20190103150805_fix_event_visibility.exs
index 7c1a3fa4..fe3e8b35 100644
--- a/priv/repo/migrations/20190103150805_fix_event_visibility.exs
+++ b/priv/repo/migrations/20190103150805_fix_event_visibility.exs
@@ -4,7 +4,7 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do
def up do
Mobilizon.Events.EventVisibility.create_type()
Mobilizon.Events.EventStatus.create_type()
- Mobilizon.Conversations.CommentVisibility.create_type()
+ Mobilizon.Discussions.CommentVisibility.create_type()
alter table(:events) do
remove(:public)
@@ -15,7 +15,7 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do
end
alter table(:comments) do
- add(:visibility, Mobilizon.Conversations.CommentVisibility.type())
+ add(:visibility, Mobilizon.Discussions.CommentVisibility.type())
end
end
@@ -34,6 +34,6 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do
Mobilizon.Events.EventVisibility.drop_type()
Mobilizon.Events.EventStatus.drop_type()
- Mobilizon.Conversations.CommentVisibility.drop_type()
+ Mobilizon.Discussions.CommentVisibility.drop_type()
end
end
diff --git a/priv/repo/migrations/20190929170817_rename_postgres_types.exs b/priv/repo/migrations/20190929170817_rename_postgres_types.exs
index a46d0d28..b418e679 100644
--- a/priv/repo/migrations/20190929170817_rename_postgres_types.exs
+++ b/priv/repo/migrations/20190929170817_rename_postgres_types.exs
@@ -2,7 +2,7 @@ defmodule Mobilizon.Storage.Repo.Migrations.RenamePostgresTypes do
use Ecto.Migration
alias Mobilizon.Actors.{ActorVisibility, MemberRole}
- alias Mobilizon.Conversations.CommentVisibility
+ alias Mobilizon.Discussions.CommentVisibility
alias Mobilizon.Events.{
JoinOptions,
diff --git a/priv/repo/migrations/20200708080516_create_posts.exs b/priv/repo/migrations/20200708080516_create_posts.exs
new file mode 100644
index 00000000..59c06844
--- /dev/null
+++ b/priv/repo/migrations/20200708080516_create_posts.exs
@@ -0,0 +1,47 @@
+defmodule Mobilizon.Repo.Migrations.CreatePosts do
+ use Ecto.Migration
+
+ alias Mobilizon.Posts.PostVisibility
+
+ def up do
+ PostVisibility.create_type()
+
+ create table(:posts, primary_key: false) do
+ add(:id, :uuid, primary_key: true)
+ add(:title, :string)
+ add(:slug, :string)
+ add(:url, :string)
+ add(:body, :text)
+ add(:draft, :boolean, default: false, null: false)
+ add(:local, :boolean, default: true, null: false)
+ add(:visibility, PostVisibility.type(), default: "public")
+ add(:publish_at, :utc_datetime)
+ add(:author_id, references(:actors, on_delete: :delete_all))
+ add(:attributed_to_id, references(:actors, on_delete: :delete_all))
+ add(:picture_id, references(:pictures, on_delete: :delete_all))
+
+ timestamps()
+ end
+
+ create table(:posts_tags, primary_key: false) do
+ add(:post_id, references(:posts, on_delete: :delete_all, type: :uuid), primary_key: true)
+ add(:tag_id, references(:tags, on_delete: :delete_all), primary_key: true)
+ end
+
+ alter table(:actors) do
+ add(:posts_url, :string, null: true)
+ add(:events_url, :string, null: true)
+ end
+ end
+
+ def down do
+ drop(table(:posts_tags))
+ drop(table(:posts))
+ PostVisibility.drop_type()
+
+ alter table(:actors) do
+ remove(:posts_url, :string, null: true)
+ remove(:events_url, :string, null: true)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs b/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs
new file mode 100644
index 00000000..293db178
--- /dev/null
+++ b/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs
@@ -0,0 +1,21 @@
+defmodule Mobilizon.Storage.Repo.Migrations.RenameConversationsToDiscussions do
+ use Ecto.Migration
+
+ def up do
+ rename(table("conversations"), to: table("discussions"))
+ rename(table("comments"), :conversation_id, to: :discussion_id)
+
+ alter table(:actors) do
+ add(:discussions_url, :string, null: true)
+ end
+ end
+
+ def down do
+ rename(table("discussions"), to: table("conversations"))
+ rename(table("comments"), :discussion_id, to: :conversation_id)
+
+ alter table(:actors) do
+ remove(:discussions_url)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200721131521_add_url_to_conversation.exs b/priv/repo/migrations/20200721131521_add_url_to_conversation.exs
new file mode 100644
index 00000000..b0c30eef
--- /dev/null
+++ b/priv/repo/migrations/20200721131521_add_url_to_conversation.exs
@@ -0,0 +1,36 @@
+defmodule Mobilizon.Storage.Repo.Migrations.AddUrlToConversation do
+ use Ecto.Migration
+
+ def up do
+ # Just in case old name is used
+ drop_if_exists(constraint(:comments, :comments_conversation_id_fkey))
+ drop_if_exists(constraint(:comments, :comments_discussion_id_fkey))
+
+ alter table(:discussions, primary_key: false) do
+ remove(:id)
+ add(:id, :uuid, primary_key: true)
+ add(:url, :string, null: false)
+ end
+
+ alter table(:comments) do
+ remove(:discussion_id)
+ add(:discussion_id, references(:discussions, type: :uuid), null: true)
+ end
+ end
+
+ def down do
+ drop_if_exists(constraint(:comments, :comments_conversation_id_fkey))
+ drop_if_exists(constraint(:comments, :comments_discussion_id_fkey))
+
+ alter table(:discussions, primary_key: true) do
+ remove(:id)
+ add(:id, :serial, primary_key: true)
+ remove(:url)
+ end
+
+ alter table(:comments) do
+ remove(:discussion_id)
+ add(:discussion_id, references(:discussions, type: :serial), null: true)
+ end
+ end
+end
diff --git a/schema.graphql b/schema.graphql
index 5db07970..ec4af4a8 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -7,21 +7,33 @@ schema {
subscription: RootSubscriptionType
}
-"""An action log"""
+"""
+An action log
+"""
type ActionLog {
- """The action that was done"""
+ """
+ The action that was done
+ """
action: ActionLogAction
- """The actor that acted"""
+ """
+ The actor that acted
+ """
actor: Actor
- """Internal ID for this comment"""
+ """
+ Internal ID for this comment
+ """
id: ID
- """The time when the action was performed"""
+ """
+ The time when the action was performed
+ """
insertedAt: DateTime
- """The object that was acted upon"""
+ """
+ The object that was acted upon
+ """
object: ActionLogObject
}
@@ -36,78 +48,128 @@ enum ActionLogAction {
REPORT_UPDATE_RESOLVED
}
-"""The objects that can be in an action log"""
+"""
+The objects that can be in an action log
+"""
interface ActionLogObject {
- """Internal ID for this object"""
+ """
+ Internal ID for this object
+ """
id: ID
}
-"""An ActivityPub actor"""
+"""
+An ActivityPub actor
+"""
interface Actor {
- """The actor's avatar picture"""
+ """
+ The actor's avatar picture
+ """
avatar: Picture
- """The actor's banner picture"""
+ """
+ The actor's banner picture
+ """
banner: Picture
- """The actor's domain if (null if it's this instance)"""
+ """
+ The actor's domain if (null if it's this instance)
+ """
domain: String
- """List of followers"""
+ """
+ List of followers
+ """
followers: [Follower]
- """Number of followers for this actor"""
+ """
+ Number of followers for this actor
+ """
followersCount: Int
- """List of followings"""
+ """
+ List of followings
+ """
following: [Follower]
- """Number of actors following this actor"""
+ """
+ Number of actors following this actor
+ """
followingCount: Int
- """Internal ID for this actor"""
+ """
+ Internal ID for this actor
+ """
id: ID
- """If the actor is from this instance"""
+ """
+ If the actor is from this instance
+ """
local: Boolean
- """Whether the actors manually approves followers"""
+ """
+ Whether the actors manually approves followers
+ """
manuallyApprovesFollowers: Boolean
- """The actor's displayed name"""
+ """
+ The actor's displayed name
+ """
name: String
- """The actor's preferred username"""
+ """
+ The actor's preferred username
+ """
preferredUsername: String
- """The actor's summary"""
+ """
+ The actor's summary
+ """
summary: String
- """If the actor is suspended"""
+ """
+ If the actor is suspended
+ """
suspended: Boolean
- """The type of Actor (Person, Group,…)"""
+ """
+ The type of Actor (Person, Group,…)
+ """
type: ActorType
- """The ActivityPub actor's URL"""
+ """
+ The ActivityPub actor's URL
+ """
url: String
}
-"""The list of types an actor can be"""
+"""
+The list of types an actor can be
+"""
enum ActorType {
- """An ActivityPub Application"""
+ """
+ An ActivityPub Application
+ """
APPLICATION
- """An ActivityPub Group"""
+ """
+ An ActivityPub Group
+ """
GROUP
- """An ActivityPub Organization"""
+ """
+ An ActivityPub Organization
+ """
ORGANIZATION
- """An ActivityPub Person"""
+ """
+ An ActivityPub Person
+ """
PERSON
- """An ActivityPub Service"""
+ """
+ An ActivityPub Service
+ """
SERVICE
}
@@ -115,17 +177,23 @@ type Address {
country: String
description: String
- """The geocoordinates for the point where this address is"""
+ """
+ The geocoordinates for the point where this address is
+ """
geom: Point
id: ID
- """The address's locality"""
+ """
+ The address's locality
+ """
locality: String
originId: String
postalCode: String
region: String
- """The address's street name (with number)"""
+ """
+ The address's street name (with number)
+ """
street: String
type: String
url: String
@@ -135,17 +203,23 @@ input AddressInput {
country: String
description: String
- """The geocoordinates for the point where this address is"""
+ """
+ The geocoordinates for the point where this address is
+ """
geom: Point
id: ID
- """The address's locality"""
+ """
+ The address's locality
+ """
locality: String
originId: String
postalCode: String
region: String
- """The address's street name (with number)"""
+ """
+ The address's street name (with number)
+ """
street: String
type: String
url: String
@@ -206,65 +280,100 @@ type AnonymousParticipationValidationEmail {
"""
Represents an application
-
"""
type Application implements Actor {
- """The actor's avatar picture"""
+ """
+ The actor's avatar picture
+ """
avatar: Picture
- """The actor's banner picture"""
+ """
+ The actor's banner picture
+ """
banner: Picture
- """The actor's domain if (null if it's this instance)"""
+ """
+ The actor's domain if (null if it's this instance)
+ """
domain: String
- """List of followers"""
+ """
+ List of followers
+ """
followers: [Follower]
- """Number of followers for this actor"""
+ """
+ Number of followers for this actor
+ """
followersCount: Int
- """List of followings"""
+ """
+ List of followings
+ """
following: [Follower]
- """Number of actors following this actor"""
+ """
+ Number of actors following this actor
+ """
followingCount: Int
- """Internal ID for this application"""
+ """
+ Internal ID for this application
+ """
id: ID
- """If the actor is from this instance"""
+ """
+ If the actor is from this instance
+ """
local: Boolean
- """Whether the actors manually approves followers"""
+ """
+ Whether the actors manually approves followers
+ """
manuallyApprovesFollowers: Boolean
- """The actor's displayed name"""
+ """
+ The actor's displayed name
+ """
name: String
- """The actor's preferred username"""
+ """
+ The actor's preferred username
+ """
preferredUsername: String
- """The actor's summary"""
+ """
+ The actor's summary
+ """
summary: String
- """If the actor is suspended"""
+ """
+ If the actor is suspended
+ """
suspended: Boolean
- """The type of Actor (Person, Group,…)"""
+ """
+ The type of Actor (Person, Group,…)
+ """
type: ActorType
- """The ActivityPub actor's URL"""
+ """
+ The ActivityPub actor's URL
+ """
url: String
}
-"""A comment"""
+"""
+A comment
+"""
type Comment implements ActionLogObject {
actor: Person
deletedAt: DateTime
event: Event
- """Internal ID for this comment"""
+ """
+ Internal ID for this comment
+ """
id: ID
inReplyToComment: Comment
insertedAt: DateTime
@@ -281,25 +390,39 @@ type Comment implements ActionLogObject {
visibility: CommentVisibility
}
-"""The list of visibility options for a comment"""
+"""
+The list of visibility options for a comment
+"""
enum CommentVisibility {
- """visible only to people invited"""
+ """
+ visible only to people invited
+ """
INVITE
- """Visible only after a moderator accepted"""
+ """
+ Visible only after a moderator accepted
+ """
MODERATED
- """Visible only to people members of the group or followers of the person"""
+ """
+ Visible only to people members of the group or followers of the person
+ """
PRIVATE
- """Publicly listed and federated. Can be shared."""
+ """
+ Publicly listed and federated. Can be shared.
+ """
PUBLIC
- """Visible only to people with the link - or invited"""
+ """
+ Visible only to people with the link - or invited
+ """
UNLISTED
}
-"""A config object"""
+"""
+A config object
+"""
type Config {
anonymous: Anonymous
countryCode: String
@@ -313,19 +436,27 @@ type Config {
registrationsWhitelist: Boolean
resourceProviders: [ResourceProvider]
- """The instance's terms"""
+ """
+ The instance's terms
+ """
terms(locale: String = "en"): Terms
}
-"""A conversation"""
-type Conversation {
+"""
+A discussion
+"""
+type discussion {
actor: Actor
- """The comments for the conversation"""
+ """
+ The comments for the discussion
+ """
comments(limit: Int = 10, page: Int = 1): PaginatedCommentList
creator: Person
- """Internal ID for this conversation"""
+ """
+ Internal ID for this discussion
+ """
id: ID
insertedAt: DateTime
lastComment: Comment
@@ -335,19 +466,29 @@ type Conversation {
}
type Dashboard {
- """Last public event publish"""
+ """
+ Last public event publish
+ """
lastPublicEventPublished: Event
- """The number of local comments"""
+ """
+ The number of local comments
+ """
numberOfComments: Int
- """The number of local events"""
+ """
+ The number of local events
+ """
numberOfEvents: Int
- """The number of current opened reports"""
+ """
+ The number of current opened reports
+ """
numberOfReports: Int
- """The number of local users"""
+ """
+ The number of local users
+ """
numberOfUsers: Int
}
@@ -359,161 +500,260 @@ be converted to UTC and any UTC offset other than 0 will be rejected.
"""
scalar DateTime
-"""Represents a deleted feed_token"""
+"""
+Represents a deleted feed_token
+"""
type DeletedFeedToken {
actor: DeletedObject
user: DeletedObject
}
-"""Represents a deleted member"""
+"""
+Represents a deleted member
+"""
type DeletedMember {
actor: DeletedObject
parent: DeletedObject
}
-"""A struct containing the id of the deleted object"""
+"""
+A struct containing the id of the deleted object
+"""
type DeletedObject {
id: ID
}
-"""Represents a deleted participant"""
+"""
+Represents a deleted participant
+"""
type DeletedParticipant {
actor: DeletedObject
event: DeletedObject
id: ID
}
-"""An event"""
+"""
+An event
+"""
type Event implements ActionLogObject {
- """Who the event is attributed to (often a group)"""
+ """
+ Who the event is attributed to (often a group)
+ """
attributedTo: Actor
- """Datetime for when the event begins"""
+ """
+ Datetime for when the event begins
+ """
beginsOn: DateTime
- """The event's category"""
+ """
+ The event's category
+ """
category: String
- """The comments in reply to the event"""
+ """
+ The comments in reply to the event
+ """
comments: [Comment]
- """When the event was created"""
+ """
+ When the event was created
+ """
createdAt: DateTime
- """The event's description"""
+ """
+ The event's description
+ """
description: String
- """Whether or not the event is a draft"""
+ """
+ Whether or not the event is a draft
+ """
draft: Boolean
- """Datetime for when the event ends"""
+ """
+ Datetime for when the event ends
+ """
endsOn: DateTime
- """Internal ID for this event"""
+ """
+ Internal ID for this event
+ """
id: ID
- """The event's visibility"""
+ """
+ The event's visibility
+ """
joinOptions: EventJoinOptions
- """Whether the event is local or not"""
+ """
+ Whether the event is local or not
+ """
local: Boolean
- """Online address of the event"""
+ """
+ Online address of the event
+ """
onlineAddress: String
- """The event options"""
+ """
+ The event options
+ """
options: EventOptions
- """The event's organizer (as a person)"""
+ """
+ The event's organizer (as a person)
+ """
organizerActor: Actor
participantStats: ParticipantStats
- """The event's participants"""
- participants(actorId: ID, limit: Int = 10, page: Int = 1, roles: String = ""): PaginatedParticipantList
+ """
+ The event's participants
+ """
+ participants(
+ actorId: ID
+ limit: Int = 10
+ page: Int = 1
+ roles: String = ""
+ ): PaginatedParticipantList
- """Phone address for the event"""
+ """
+ Phone address for the event
+ """
phoneAddress: String
- """The type of the event's address"""
+ """
+ The type of the event's address
+ """
physicalAddress: Address
- """The event's picture"""
+ """
+ The event's picture
+ """
picture: Picture
- """When the event was published"""
+ """
+ When the event was published
+ """
publishAt: DateTime
- """Events related to this one"""
+ """
+ Events related to this one
+ """
relatedEvents: [Event]
- """The event's description's slug"""
+ """
+ The event's description's slug
+ """
slug: String
- """Status of the event"""
+ """
+ Status of the event
+ """
status: EventStatus
- """The event's tags"""
+ """
+ The event's tags
+ """
tags: [Tag]
- """The event's title"""
+ """
+ The event's title
+ """
title: String
- """When the event was last updated"""
+ """
+ When the event was last updated
+ """
updatedAt: DateTime
- """The ActivityPub Event URL"""
+ """
+ The ActivityPub Event URL
+ """
url: String
- """The Event UUID"""
+ """
+ The Event UUID
+ """
uuid: UUID
- """The event's visibility"""
+ """
+ The event's visibility
+ """
visibility: EventVisibility
}
-"""The list of possible options for the event's status"""
+"""
+The list of possible options for the event's status
+"""
enum EventCommentModeration {
- """Anyone can comment under the event"""
+ """
+ Anyone can comment under the event
+ """
ALLOW_ALL
- """No one can comment except for the admin"""
+ """
+ No one can comment except for the admin
+ """
CLOSED
- """Every comment has to be moderated by the admin"""
+ """
+ Every comment has to be moderated by the admin
+ """
MODERATED
}
-"""The list of join options for an event"""
+"""
+The list of join options for an event
+"""
enum EventJoinOptions {
- """Anyone can join and is automatically accepted"""
+ """
+ Anyone can join and is automatically accepted
+ """
FREE
- """Participants must be invited"""
+ """
+ Participants must be invited
+ """
INVITE
- """Manual acceptation"""
+ """
+ Manual acceptation
+ """
RESTRICTED
}
type EventOffer {
- """The price amount for this offer"""
+ """
+ The price amount for this offer
+ """
price: Float
- """The currency for this price offer"""
+ """
+ The currency for this price offer
+ """
priceCurrency: String
- """The URL to access to this offer"""
+ """
+ The URL to access to this offer
+ """
url: String
}
input EventOfferInput {
- """The price amount for this offer"""
+ """
+ The price amount for this offer
+ """
price: Float
- """The currency for this price offer"""
+ """
+ The currency for this price offer
+ """
priceCurrency: String
- """The URL to access to this offer"""
+ """
+ The URL to access to this offer
+ """
url: String
}
@@ -523,10 +763,14 @@ type EventOptions {
"""
anonymousParticipation: Boolean
- """The list of special attendees"""
+ """
+ The list of special attendees
+ """
attendees: [String]
- """The policy on public comment moderation under the event"""
+ """
+ The policy on public comment moderation under the event
+ """
commentModeration: EventCommentModeration
"""
@@ -534,31 +778,49 @@ type EventOptions {
"""
hideOrganizerWhenGroupEvent: Boolean
- """The maximum attendee capacity for this event"""
+ """
+ The maximum attendee capacity for this event
+ """
maximumAttendeeCapacity: Int
- """The list of offers to show for this event"""
+ """
+ The list of offers to show for this event
+ """
offers: [EventOffer]
- """The list of participation conditions to accept to join this event"""
+ """
+ The list of participation conditions to accept to join this event
+ """
participationConditions: [EventParticipationCondition]
- """The list of the event"""
+ """
+ The list of the event
+ """
program: String
- """The number of remaining seats for this event"""
+ """
+ The number of remaining seats for this event
+ """
remainingAttendeeCapacity: Int
- """Show event end time"""
+ """
+ Show event end time
+ """
showEndTime: Boolean
- """Whether or not to show the participation price"""
+ """
+ Whether or not to show the participation price
+ """
showParticipationPrice: Boolean
- """Whether or not to show the number of remaining seats for this event"""
+ """
+ Whether or not to show the number of remaining seats for this event
+ """
showRemainingAttendeeCapacity: Boolean
- """Show event start time"""
+ """
+ Show event start time
+ """
showStartTime: Boolean
}
@@ -568,10 +830,14 @@ input EventOptionsInput {
"""
anonymousParticipation: Boolean = false
- """The list of special attendees"""
+ """
+ The list of special attendees
+ """
attendees: [String]
- """The policy on public comment moderation under the event"""
+ """
+ The policy on public comment moderation under the event
+ """
commentModeration: EventCommentModeration
"""
@@ -579,122 +845,193 @@ input EventOptionsInput {
"""
hideOrganizerWhenGroupEvent: Boolean
- """The maximum attendee capacity for this event"""
+ """
+ The maximum attendee capacity for this event
+ """
maximumAttendeeCapacity: Int
- """The list of offers to show for this event"""
+ """
+ The list of offers to show for this event
+ """
offers: [EventOfferInput]
- """The list of participation conditions to accept to join this event"""
+ """
+ The list of participation conditions to accept to join this event
+ """
participationConditions: [EventParticipationConditionInput]
- """The list of the event"""
+ """
+ The list of the event
+ """
program: String
- """The number of remaining seats for this event"""
+ """
+ The number of remaining seats for this event
+ """
remainingAttendeeCapacity: Int
- """Show event end time"""
+ """
+ Show event end time
+ """
showEndTime: Boolean
- """Whether or not to show the participation price"""
+ """
+ Whether or not to show the participation price
+ """
showParticipationPrice: Boolean
- """Whether or not to show the number of remaining seats for this event"""
+ """
+ Whether or not to show the number of remaining seats for this event
+ """
showRemainingAttendeeCapacity: Boolean
- """Show event start time"""
+ """
+ Show event start time
+ """
showStartTime: Boolean
}
type EventParticipationCondition {
- """The content for this condition"""
+ """
+ The content for this condition
+ """
content: String
- """The title for this condition"""
+ """
+ The title for this condition
+ """
title: String
- """The URL to access this condition"""
+ """
+ The URL to access this condition
+ """
url: String
}
input EventParticipationConditionInput {
- """The content for this condition"""
+ """
+ The content for this condition
+ """
content: String
- """The title for this condition"""
+ """
+ The title for this condition
+ """
title: String
- """The URL to access this condition"""
+ """
+ The URL to access this condition
+ """
url: String
}
-"""Search events result"""
+"""
+Search events result
+"""
type Events {
- """Event elements"""
+ """
+ Event elements
+ """
elements: [Event]!
- """Total elements"""
+ """
+ Total elements
+ """
total: Int!
}
-"""The list of possible options for the event's status"""
+"""
+The list of possible options for the event's status
+"""
enum EventStatus {
- """The event is cancelled"""
+ """
+ The event is cancelled
+ """
CANCELLED
- """The event is confirmed"""
+ """
+ The event is confirmed
+ """
CONFIRMED
- """The event is tentative"""
+ """
+ The event is tentative
+ """
TENTATIVE
}
-"""The list of visibility options for an event"""
+"""
+The list of visibility options for an event
+"""
enum EventVisibility {
- """Visible only to people members of the group or followers of the person"""
+ """
+ Visible only to people members of the group or followers of the person
+ """
PRIVATE
- """Publicly listed and federated. Can be shared."""
+ """
+ Publicly listed and federated. Can be shared.
+ """
PUBLIC
- """Visible only after a moderator accepted"""
+ """
+ Visible only after a moderator accepted
+ """
RESTRICTED
- """Visible only to people with the link - or invited"""
+ """
+ Visible only to people with the link - or invited
+ """
UNLISTED
}
-"""Represents a participant to an event"""
+"""
+Represents a participant to an event
+"""
type FeedToken {
- """The event which the actor participates in"""
+ """
+ The event which the actor participates in
+ """
actor: Actor
- """The role of this actor at this event"""
+ """
+ The role of this actor at this event
+ """
token: String
- """The actor that participates to the event"""
+ """
+ The actor that participates to the event
+ """
user: User
}
"""
Represents an actor's follower
-
"""
type Follower {
- """Which profile follows"""
+ """
+ Which profile follows
+ """
actor: Actor
- """Whether the follow has been approved by the target actor"""
+ """
+ Whether the follow has been approved by the target actor
+ """
approved: Boolean
- """When the follow was created"""
+ """
+ When the follow was created
+ """
insertedAt: DateTime
- """What or who the profile follows"""
+ """
+ What or who the profile follows
+ """
targetActor: Actor
- """When the follow was updated"""
+ """
+ When the follow was updated
+ """
updatedAt: DateTime
}
@@ -705,97 +1042,151 @@ type Geocoding {
"""
Represents a group of actors
-
"""
type Group implements Actor {
- """The actor's avatar picture"""
+ """
+ The actor's avatar picture
+ """
avatar: Picture
- """The actor's banner picture"""
+ """
+ The actor's banner picture
+ """
banner: Picture
- """A list of the conversations for this group"""
- conversations: PaginatedConversationList
+ """
+ A list of the discussions for this group
+ """
+ discussions: PaginatedDiscussionList
- """The actor's domain if (null if it's this instance)"""
+ """
+ The actor's domain if (null if it's this instance)
+ """
domain: String
- """List of followers"""
+ """
+ List of followers
+ """
followers: [Follower]
- """Number of followers for this actor"""
+ """
+ Number of followers for this actor
+ """
followersCount: Int
- """List of followings"""
+ """
+ List of followings
+ """
following: [Follower]
- """Number of actors following this actor"""
+ """
+ Number of actors following this actor
+ """
followingCount: Int
- """Internal ID for this group"""
+ """
+ Internal ID for this group
+ """
id: ID
- """If the actor is from this instance"""
+ """
+ If the actor is from this instance
+ """
local: Boolean
- """Whether the actors manually approves followers"""
+ """
+ Whether the actors manually approves followers
+ """
manuallyApprovesFollowers: Boolean
- """List of group members"""
+ """
+ List of group members
+ """
members: PaginatedMemberList
- """The actor's displayed name"""
+ """
+ The actor's displayed name
+ """
name: String
- """Whether the group is opened to all or has restricted access"""
+ """
+ Whether the group is opened to all or has restricted access
+ """
openness: Openness
- """A list of the events this actor has organized"""
+ """
+ A list of the events this actor has organized
+ """
organizedEvents: PaginatedEventList
- """The actor's preferred username"""
+ """
+ The actor's preferred username
+ """
preferredUsername: String
- """A paginated list of the resources this group has"""
+ """
+ A paginated list of the resources this group has
+ """
resources(limit: Int = 10, page: Int = 1): PaginatedResourceList
- """The actor's summary"""
+ """
+ The actor's summary
+ """
summary: String
- """If the actor is suspended"""
+ """
+ If the actor is suspended
+ """
suspended: Boolean
- """A paginated list of the todo lists this group has"""
+ """
+ A paginated list of the todo lists this group has
+ """
todoLists: PaginatedTodoListList
- """The type of Actor (Person, Group,…)"""
+ """
+ The type of Actor (Person, Group,…)
+ """
type: ActorType
- """The type of group : Group, Community,…"""
+ """
+ The type of group : Group, Community,…
+ """
types: GroupType
- """The ActivityPub actor's URL"""
+ """
+ The ActivityPub actor's URL
+ """
url: String
}
-"""Search groups result"""
+"""
+Search groups result
+"""
type Groups {
- """Group elements"""
+ """
+ Group elements
+ """
elements: [Group]!
- """Total elements"""
+ """
+ Total elements
+ """
total: Int!
}
"""
The types of Group that exist
-
"""
enum GroupType {
- """A public group of many actors"""
+ """
+ A public group of many actors
+ """
COMMUNITY
- """A private group of persons"""
+ """
+ A private group of persons
+ """
GROUP
}
@@ -805,15 +1196,23 @@ enum InstanceTermsType {
URL
}
-"""A JWT and the associated user ID"""
+"""
+A JWT and the associated user ID
+"""
type Login {
- """A JWT Token for this session"""
+ """
+ A JWT Token for this session
+ """
accessToken: String!
- """A JWT Token to refresh the access token"""
+ """
+ A JWT Token to refresh the access token
+ """
refreshToken: String!
- """The user associated to this session"""
+ """
+ The user associated to this session
+ """
user: User!
}
@@ -829,19 +1228,26 @@ type Maps {
"""
Represents a member of a group
-
"""
type Member {
- """Which profile is member of"""
+ """
+ Which profile is member of
+ """
actor: Person
- """The member's ID"""
+ """
+ The member's ID
+ """
id: ID
- """Of which the profile is member"""
+ """
+ Of which the profile is member
+ """
parent: Group
- """The role of this membership"""
+ """
+ The role of this membership
+ """
role: MemberRoleEnum
}
@@ -864,125 +1270,188 @@ scalar NaiveDateTime
"""
Describes how an actor is opened to follows
-
"""
enum Openness {
- """The actor can only be followed by invitation"""
+ """
+ The actor can only be followed by invitation
+ """
INVITE_ONLY
- """The actor needs to accept the following before it's effective"""
+ """
+ The actor needs to accept the following before it's effective
+ """
MODERATED
- """The actor is open to followings"""
+ """
+ The actor is open to followings
+ """
OPEN
}
type PaginatedCommentList {
- """A list of comments"""
+ """
+ A list of comments
+ """
elements: [Comment]
- """The total number of comments in the list"""
+ """
+ The total number of comments in the list
+ """
total: Int
}
-type PaginatedConversationList {
- """A list of conversation"""
- elements: [Conversation]
+type PaginatedDiscussionList {
+ """
+ A list of discussion
+ """
+ elements: [discussion]
- """The total number of comments in the list"""
+ """
+ The total number of comments in the list
+ """
total: Int
}
type PaginatedEventList {
- """A list of events"""
+ """
+ A list of events
+ """
elements: [Event]
- """The total number of events in the list"""
+ """
+ The total number of events in the list
+ """
total: Int
}
type PaginatedFollowerList {
- """A list of followers"""
+ """
+ A list of followers
+ """
elements: [Follower]
- """The total number of elements in the list"""
+ """
+ The total number of elements in the list
+ """
total: Int
}
type PaginatedGroupList {
- """A list of groups"""
+ """
+ A list of groups
+ """
elements: [Group]
- """The total number of elements in the list"""
+ """
+ The total number of elements in the list
+ """
total: Int
}
type PaginatedMemberList {
- """A list of members"""
+ """
+ A list of members
+ """
elements: [Member]
- """The total number of elements in the list"""
+ """
+ The total number of elements in the list
+ """
total: Int
}
type PaginatedParticipantList {
- """A list of participants"""
+ """
+ A list of participants
+ """
elements: [Participant]
- """The total number of participants in the list"""
+ """
+ The total number of participants in the list
+ """
total: Int
}
type PaginatedResourceList {
- """A list of resources"""
+ """
+ A list of resources
+ """
elements: [Resource]
- """The total number of resources in the list"""
+ """
+ The total number of resources in the list
+ """
total: Int
}
type PaginatedTodoList {
- """A list of todos"""
+ """
+ A list of todos
+ """
elements: [Todo]
- """The total number of todos in the list"""
+ """
+ The total number of todos in the list
+ """
total: Int
}
type PaginatedTodoListList {
- """A list of todo lists"""
+ """
+ A list of todo lists
+ """
elements: [TodoList]
- """The total number of todo lists in the list"""
+ """
+ The total number of todo lists in the list
+ """
total: Int
}
-"""Represents a participant to an event"""
+"""
+Represents a participant to an event
+"""
type Participant {
- """The actor that participates to the event"""
+ """
+ The actor that participates to the event
+ """
actor: Actor
- """The event which the actor participates in"""
+ """
+ The event which the actor participates in
+ """
event: Event
- """The participation ID"""
+ """
+ The participation ID
+ """
id: ID
- """The datetime this participant was created"""
+ """
+ The datetime this participant was created
+ """
insertedAt: DateTime
- """The metadata associated to this participant"""
+ """
+ The metadata associated to this participant
+ """
metadata: ParticipantMetadata
- """The role of this actor at this event"""
+ """
+ The role of this actor at this event
+ """
role: ParticipantRoleEnum
}
type ParticipantMetadata {
- """The eventual token to leave an event when user is anonymous"""
+ """
+ The eventual token to leave an event when user is anonymous
+ """
cancellationToken: String
- """The eventual message the participant left"""
+ """
+ The eventual message the participant left
+ """
message: String
}
@@ -997,140 +1466,223 @@ enum ParticipantRoleEnum {
}
type ParticipantStats {
- """The number of administrators"""
+ """
+ The number of administrators
+ """
administrator: Int
- """The number of creators"""
+ """
+ The number of creators
+ """
creator: Int
- """The number of approved participants"""
+ """
+ The number of approved participants
+ """
going: Int
- """The number of moderators"""
+ """
+ The number of moderators
+ """
moderator: Int
- """The number of not approved participants"""
+ """
+ The number of not approved participants
+ """
notApproved: Int
- """The number of not confirmed participants"""
+ """
+ The number of not confirmed participants
+ """
notConfirmed: Int
- """The number of simple participants (excluding creators)"""
+ """
+ The number of simple participants (excluding creators)
+ """
participant: Int
- """The number of rejected participants"""
+ """
+ The number of rejected participants
+ """
rejected: Int
}
"""
Represents a person identity
-
"""
type Person implements Actor {
- """The actor's avatar picture"""
+ """
+ The actor's avatar picture
+ """
avatar: Picture
- """The actor's banner picture"""
+ """
+ The actor's banner picture
+ """
banner: Picture
- """The actor's domain if (null if it's this instance)"""
+ """
+ The actor's domain if (null if it's this instance)
+ """
domain: String
- """A list of the feed tokens for this person"""
+ """
+ A list of the feed tokens for this person
+ """
feedTokens: [FeedToken]
- """List of followers"""
+ """
+ List of followers
+ """
followers: [Follower]
- """Number of followers for this actor"""
+ """
+ Number of followers for this actor
+ """
followersCount: Int
- """List of followings"""
+ """
+ List of followings
+ """
following: [Follower]
- """Number of actors following this actor"""
+ """
+ Number of actors following this actor
+ """
followingCount: Int
- """Internal ID for this person"""
+ """
+ Internal ID for this person
+ """
id: ID
- """If the actor is from this instance"""
+ """
+ If the actor is from this instance
+ """
local: Boolean
- """Whether the actors manually approves followers"""
+ """
+ Whether the actors manually approves followers
+ """
manuallyApprovesFollowers: Boolean
- """The list of groups this person is member of"""
+ """
+ The list of groups this person is member of
+ """
memberOf: [Member]
- """The list of group this person is member of"""
+ """
+ The list of group this person is member of
+ """
memberships: PaginatedMemberList
- """The actor's displayed name"""
+ """
+ The actor's displayed name
+ """
name: String
- """A list of the events this actor has organized"""
+ """
+ A list of the events this actor has organized
+ """
organizedEvents: [Event]
- """The list of events this person goes to"""
+ """
+ The list of events this person goes to
+ """
participations(eventId: ID): [Participant]
- """The actor's preferred username"""
+ """
+ The actor's preferred username
+ """
preferredUsername: String
- """The actor's summary"""
+ """
+ The actor's summary
+ """
summary: String
- """If the actor is suspended"""
+ """
+ If the actor is suspended
+ """
suspended: Boolean
- """The type of Actor (Person, Group,…)"""
+ """
+ The type of Actor (Person, Group,…)
+ """
type: ActorType
- """The ActivityPub actor's URL"""
+ """
+ The ActivityPub actor's URL
+ """
url: String
- """The user this actor is associated to"""
+ """
+ The user this actor is associated to
+ """
user: User
}
-"""Search persons result"""
+"""
+Search persons result
+"""
type Persons {
- """Person elements"""
+ """
+ Person elements
+ """
elements: [Person]!
- """Total elements"""
+ """
+ Total elements
+ """
total: Int!
}
-"""A picture"""
+"""
+A picture
+"""
type Picture {
- """The picture's alternative text"""
+ """
+ The picture's alternative text
+ """
alt: String
- """The picture's detected content type"""
+ """
+ The picture's detected content type
+ """
contentType: String
- """The picture's ID"""
+ """
+ The picture's ID
+ """
id: ID
- """The picture's name"""
+ """
+ The picture's name
+ """
name: String
- """The picture's size"""
+ """
+ The picture's size
+ """
size: Int
- """The picture's full URL"""
+ """
+ The picture's full URL
+ """
url: String
}
-"""An attached picture or a link to a picture"""
+"""
+An attached picture or a link to a picture
+"""
input PictureInput {
picture: PictureInputObject
pictureId: ID
}
-"""An attached picture"""
+"""
+An attached picture
+"""
input PictureInputObject {
actorId: ID
alt: String
@@ -1139,128 +1691,208 @@ input PictureInputObject {
}
"""
-The `Point` scalar type represents Point geographic information compliant string data,
+The `Point` scalar type represents Point geographic information compliant string data,
represented as floats separated by a semi-colon. The geodetic system is WGS 84
"""
scalar Point
-"""Token"""
+"""
+Token
+"""
type RefreshedToken {
- """Generated access token"""
+ """
+ Generated access token
+ """
accessToken: String!
- """Generated refreshed token"""
+ """
+ Generated refreshed token
+ """
refreshToken: String!
}
-"""A report object"""
+"""
+A report object
+"""
type Report implements ActionLogObject {
- """The comments that are reported"""
+ """
+ The comments that are reported
+ """
comments: [Comment]
- """The comment the reporter added about this report"""
+ """
+ The comment the reporter added about this report
+ """
content: String
- """The event that is being reported"""
+ """
+ The event that is being reported
+ """
event: Event
- """The internal ID of the report"""
+ """
+ The internal ID of the report
+ """
id: ID
- """When the report was created"""
+ """
+ When the report was created
+ """
insertedAt: DateTime
- """The notes made on the event"""
+ """
+ The notes made on the event
+ """
notes: [ReportNote]
- """The actor that is being reported"""
+ """
+ The actor that is being reported
+ """
reported: Actor
- """The actor that created the report"""
+ """
+ The actor that created the report
+ """
reporter: Actor
- """Whether the report is still active"""
+ """
+ Whether the report is still active
+ """
status: ReportStatus
- """When the report was updated"""
+ """
+ When the report was updated
+ """
updatedAt: DateTime
- """The URI of the report"""
+ """
+ The URI of the report
+ """
uri: String
}
-"""A report note object"""
+"""
+A report note object
+"""
type ReportNote implements ActionLogObject {
- """The content of the note"""
+ """
+ The content of the note
+ """
content: String
- """The internal ID of the report note"""
+ """
+ The internal ID of the report note
+ """
id: ID
- """When the report note was created"""
+ """
+ When the report note was created
+ """
insertedAt: DateTime
- """The moderator who added the note"""
+ """
+ The moderator who added the note
+ """
moderator: Actor
- """The report on which this note is added"""
+ """
+ The report on which this note is added
+ """
report: Report
}
-"""The list of possible statuses for a report object"""
+"""
+The list of possible statuses for a report object
+"""
enum ReportStatus {
- """The report has been closed"""
+ """
+ The report has been closed
+ """
CLOSED
- """The report has been opened"""
+ """
+ The report has been opened
+ """
OPEN
- """The report has been marked as resolved"""
+ """
+ The report has been marked as resolved
+ """
RESOLVED
}
-"""A resource"""
+"""
+A resource
+"""
type Resource {
- """The resource's owner"""
+ """
+ The resource's owner
+ """
actor: Actor
- """Children resources in folder"""
+ """
+ Children resources in folder
+ """
children: PaginatedResourceList
- """The resource's creator"""
+ """
+ The resource's creator
+ """
creator: Actor
- """The resource's ID"""
+ """
+ The resource's ID
+ """
id: ID
- """The resource's creation date"""
+ """
+ The resource's creation date
+ """
insertedAt: NaiveDateTime
- """The resource's metadata"""
+ """
+ The resource's metadata
+ """
metadata: ResourceMetadata
- """The resource's parent"""
+ """
+ The resource's parent
+ """
parent: Resource
- """The resource's path"""
+ """
+ The resource's path
+ """
path: String
- """The resource's URL"""
+ """
+ The resource's URL
+ """
resourceUrl: String
- """The resource's summary"""
+ """
+ The resource's summary
+ """
summary: String
- """The resource's title"""
+ """
+ The resource's title
+ """
title: String
- """The resource's type (if it's a folder)"""
+ """
+ The resource's type (if it's a folder)
+ """
type: String
- """The resource's last update date"""
+ """
+ The resource's last update date
+ """
updatedAt: NaiveDateTime
- """The resource's URL"""
+ """
+ The resource's URL
+ """
url: String
}
@@ -1268,21 +1900,29 @@ type ResourceMetadata {
authorName: String
authorUrl: String
- """The resource's metadata description"""
+ """
+ The resource's metadata description
+ """
description: String
faviconUrl: String
height: Int
html: String
- """The resource's metadata image"""
+ """
+ The resource's metadata image
+ """
imageRemoteUrl: String
providerName: String
providerUrl: String
- """The resource's metadata title"""
+ """
+ The resource's metadata title
+ """
title: String
- """The type of the resource"""
+ """
+ The type of the resource
+ """
type: String
width: Int
}
@@ -1294,10 +1934,14 @@ type ResourceProvider {
}
type RootMutationType {
- """Create an user"""
+ """
+ Create an user
+ """
createUser(email: String!, locale: String, password: String!): User
- """Register a first profile on registration"""
+ """
+ Register a first profile on registration
+ """
registerPerson(
"""
The avatar for the profile, either as an object or directly the ID of an existing Picture
@@ -1309,52 +1953,98 @@ type RootMutationType {
"""
banner: PictureInput
- """The email from the user previously created"""
+ """
+ The email from the user previously created
+ """
email: String!
- """The displayed name for the new profile"""
+ """
+ The displayed name for the new profile
+ """
name: String = ""
preferredUsername: String!
- """The summary for the new profile"""
+ """
+ The summary for the new profile
+ """
summary: String = ""
): Person
- """Join a group"""
+ """
+ Join a group
+ """
joinGroup(actorId: ID!, groupId: ID!): Member
- """Create a Feed Token"""
+ """
+ Create a Feed Token
+ """
createFeedToken(actorId: ID): FeedToken
- """Get a preview for a resource link"""
+ """
+ Get a preview for a resource link
+ """
previewResourceLink(resourceUrl: String!): ResourceMetadata
- """Create a note on a report"""
+ """
+ Create a note on a report
+ """
createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote
- """Create a conversation"""
- createConversation(actorId: ID!, creatorId: ID!, text: String!, title: String!): Conversation
+ """
+ Create a discussion
+ """
+ createDiscussion(
+ actorId: ID!
+ creatorId: ID!
+ text: String!
+ title: String!
+ ): discussion
inviteMember(actorId: ID!, groupId: ID!, targetActorUsername: String!): Member
- """Leave an event"""
+ """
+ Leave an event
+ """
leaveEvent(actorId: ID!, eventId: ID!, token: String): DeletedParticipant
- """Login an user"""
+ """
+ Login an user
+ """
login(email: String!, password: String!): Login
- """Change default actor for user"""
+ """
+ Change default actor for user
+ """
changeDefaultActor(preferredUsername: String!): User
- """Join an event"""
- joinEvent(actorId: ID!, email: String, eventId: ID!, message: String): Participant
+ """
+ Join an event
+ """
+ joinEvent(
+ actorId: ID!
+ email: String
+ eventId: ID!
+ message: String
+ ): Participant
- """Create a todo"""
- createTodo(assignedToId: ID, dueDate: DateTime, status: Boolean, title: String!, todoListId: ID!): Todo
+ """
+ Create a todo
+ """
+ createTodo(
+ assignedToId: ID
+ dueDate: DateTime
+ status: Boolean
+ title: String!
+ todoListId: ID!
+ ): Todo
- """Change an user password"""
+ """
+ Change an user password
+ """
changePassword(newPassword: String!, oldPassword: String!): User
- """Create a new person for user"""
+ """
+ Create a new person for user
+ """
createPerson(
"""
The avatar for the profile, either as an object or directly the ID of an existing Picture
@@ -1366,29 +2056,43 @@ type RootMutationType {
"""
banner: PictureInput
- """The displayed name for the new profile"""
+ """
+ The displayed name for the new profile
+ """
name: String = ""
preferredUsername: String!
- """The summary for the new profile"""
+ """
+ The summary for the new profile
+ """
summary: String = ""
): Person
- replyToConversation(conversationId: ID!, text: String!): Conversation
+ replyToDiscussion(discussionId: ID!, text: String!): discussion
- """Resend registration confirmation token"""
+ """
+ Resend registration confirmation token
+ """
resendConfirmationEmail(email: String!, locale: String): String
- """Create a todo list"""
+ """
+ Create a todo list
+ """
createTodoList(groupId: ID!, title: String!): TodoList
- """Delete an event"""
+ """
+ Delete an event
+ """
deleteEvent(actorId: ID!, eventId: ID!): DeletedObject
- """Change an user email"""
+ """
+ Change an user email
+ """
changeEmail(email: String!, password: String!): User
- updateConversation(conversationId: ID!, title: String!): Conversation
+ updateDiscussion(discussionId: ID!, title: String!): discussion
- """Create an event"""
+ """
+ Create an event
+ """
createEvent(
attributedToId: ID
beginsOn: DateTime!
@@ -1410,47 +2114,103 @@ type RootMutationType {
publishAt: DateTime
status: EventStatus
- """The list of tags associated to the event"""
+ """
+ The list of tags associated to the event
+ """
tags: [String] = [""]
title: String!
visibility: EventVisibility = PUBLIC
): Event
- """Delete a feed token"""
+ """
+ Delete a feed token
+ """
deleteFeedToken(token: String!): DeletedFeedToken
- """Delete a resource"""
+ """
+ Delete a resource
+ """
deleteResource(id: ID!): DeletedObject
- """Reset user password"""
+ """
+ Reset user password
+ """
resetPassword(locale: String = "en", password: String!, token: String!): Login
deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject
- """Delete an identity"""
+ """
+ Delete an identity
+ """
deletePerson(id: ID!): Person
- """Upload a picture"""
- uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture
+ """
+ Upload a picture
+ """
+ uploadPicture(
+ actorId: ID!
+ alt: String
+ file: Upload!
+ name: String!
+ ): Picture
- """Refresh a token"""
+ """
+ Refresh a token
+ """
refreshToken(refreshToken: String!): RefreshedToken
- """Delete a group"""
+ """
+ Delete a group
+ """
deleteGroup(actorId: ID!, groupId: ID!): DeletedObject
- """Update a resource"""
- updateResource(actorId: ID, id: ID!, parentId: ID, path: String, resourceUrl: String, summary: String, title: String): Resource
+ """
+ Update a resource
+ """
+ updateResource(
+ actorId: ID
+ id: ID!
+ parentId: ID
+ path: String
+ resourceUrl: String
+ summary: String
+ title: String
+ ): Resource
- """Create a comment"""
- createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment
+ """
+ Create a comment
+ """
+ createComment(
+ actorId: ID!
+ eventId: ID
+ inReplyToCommentId: ID
+ text: String!
+ ): Comment
- """Create a resource"""
- createResource(actorId: ID!, parentId: ID, path: String! = "/", resourceUrl: String, summary: String, title: String, type: String): Resource
+ """
+ Create a resource
+ """
+ createResource(
+ actorId: ID!
+ parentId: ID
+ path: String! = "/"
+ resourceUrl: String
+ summary: String
+ title: String
+ type: String
+ ): Resource
- """Update a report"""
- updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report
+ """
+ Update a report
+ """
+ updateReportStatus(
+ moderatorId: ID!
+ reportId: ID!
+ status: ReportStatus!
+ ): Report
- """Update an event"""
+ """
+ Update an event
+ """
updateEvent(
attributedToId: ID
beginsOn: DateTime
@@ -1472,45 +2232,80 @@ type RootMutationType {
picture: PictureInput
status: EventStatus
- """The list of tags associated to the event"""
+ """
+ The list of tags associated to the event
+ """
tags: [String]
title: String
visibility: EventVisibility = PUBLIC
): Event
- """Confirm a participation"""
+ """
+ Confirm a participation
+ """
confirmParticipation(confirmationToken: String!): Participant
- deleteConversation(conversationId: ID!): Conversation
+ deleteDiscussion(discussionId: ID!): discussion
- """Reject a relay subscription"""
+ """
+ Reject a relay subscription
+ """
rejectRelay(address: String!): Follower
- """Accept a participation"""
- updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant
+ """
+ Accept a participation
+ """
+ updateParticipation(
+ id: ID!
+ moderatorActorId: ID!
+ role: ParticipantRoleEnum!
+ ): Participant
- """Delete a todo"""
+ """
+ Delete a todo
+ """
deleteTodo(id: ID!): DeletedObject
deleteComment(actorId: ID!, commentId: ID!): Comment
- """Validate an user email"""
+ """
+ Validate an user email
+ """
validateEmail(token: String!): User
- """Send a link through email to reset user password"""
+ """
+ Send a link through email to reset user password
+ """
sendResetPassword(email: String!, locale: String): String
- """Update a comment"""
+ """
+ Update a comment
+ """
updateComment(commentId: ID!, text: String!): Comment
- """Accept a relay subscription"""
+ """
+ Accept a relay subscription
+ """
acceptRelay(address: String!): Follower
- """Create a report"""
- createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report
+ """
+ Create a report
+ """
+ createReport(
+ commentsIds: [ID] = [""]
+ content: String
+ eventId: ID
+ forward: Boolean = false
+ reportedId: ID!
+ reporterId: ID!
+ ): Report
- """Delete a relay subscription"""
+ """
+ Delete a relay subscription
+ """
removeRelay(address: String!): Follower
- """Update an identity"""
+ """
+ Update an identity
+ """
updatePerson(
"""
The avatar for the profile, either as an object or directly the ID of an existing Picture
@@ -1523,23 +2318,35 @@ type RootMutationType {
banner: PictureInput
id: ID!
- """The displayed name for this profile"""
+ """
+ The displayed name for this profile
+ """
name: String
- """The summary for this profile"""
+ """
+ The summary for this profile
+ """
summary: String
): Person
- """Validate an user after registration"""
+ """
+ Validate an user after registration
+ """
validateUser(token: String!): Login
- """Leave an event"""
+ """
+ Leave an event
+ """
leaveGroup(actorId: ID!, groupId: ID!): DeletedMember
- """Delete an account"""
+ """
+ Delete an account
+ """
deleteAccount(password: String!): DeletedObject
- """Create a group"""
+ """
+ Create a group
+ """
createGroup(
"""
The avatar for the group, either as an object or directly the ID of an existing Picture
@@ -1551,145 +2358,258 @@ type RootMutationType {
"""
banner: PictureInput
- """The identity that creates the group"""
+ """
+ The identity that creates the group
+ """
creatorActorId: ID!
- """The displayed name for the group"""
+ """
+ The displayed name for the group
+ """
name: String
- """The name for the group"""
+ """
+ The name for the group
+ """
preferredUsername: String!
- """The summary for the group"""
+ """
+ The summary for the group
+ """
summary: String = ""
): Group
- """Add a relay subscription"""
+ """
+ Add a relay subscription
+ """
addRelay(address: String!): Follower
- saveAdminSettings(instanceDescription: String, instanceName: String, instanceTerms: String, instanceTermsType: InstanceTermsType, instanceTermsUrl: String, registrationsOpen: Boolean): AdminSettings
+ saveAdminSettings(
+ instanceDescription: String
+ instanceName: String
+ instanceTerms: String
+ instanceTermsType: InstanceTermsType
+ instanceTermsUrl: String
+ registrationsOpen: Boolean
+ ): AdminSettings
- """Update a todo"""
- updateTodo(assignedToId: ID, dueDate: DateTime, id: ID!, status: Boolean, title: String, todoListId: ID): Todo
+ """
+ Update a todo
+ """
+ updateTodo(
+ assignedToId: ID
+ dueDate: DateTime
+ id: ID!
+ status: Boolean
+ title: String
+ todoListId: ID
+ ): Todo
}
"""
Root Query
-
"""
type RootQueryType {
- """Get the list of action logs"""
+ """
+ Get the list of action logs
+ """
actionLogs(limit: Int = 10, page: Int = 1): [ActionLog]
adminSettings: AdminSettings
- """Get the instance config"""
+ """
+ Get the instance config
+ """
config: Config
- """Get a conversation"""
- conversation(id: ID!): Conversation
+ """
+ Get a discussion
+ """
+ discussion(id: ID!): discussion
dashboard: Dashboard
- """Get an event by uuid"""
+ """
+ Get an event by uuid
+ """
event(uuid: UUID!): Event
- """Get all events"""
+ """
+ Get all events
+ """
events(limit: Int = 10, page: Int = 1): [Event]
- """Get a person by its (federated) username"""
+ """
+ Get a person by its (federated) username
+ """
fetchPerson(preferredUsername: String!): Person
- """Get a group by its preferred username"""
+ """
+ Get a group by its preferred username
+ """
group(preferredUsername: String!): Group
- """Get all groups"""
+ """
+ Get all groups
+ """
groups(limit: Int = 10, page: Int = 1): PaginatedGroupList
- """Get the persons for an user"""
+ """
+ Get the persons for an user
+ """
identities: [Person]
- """Get the current actor for the logged-in user"""
+ """
+ Get the current actor for the logged-in user
+ """
loggedPerson: Person
- """Get the current user"""
+ """
+ Get the current user
+ """
loggedUser: User
- """Get a person by its ID"""
+ """
+ Get a person by its ID
+ """
person(id: ID!): Person
- """Get a picture"""
+ """
+ Get a picture
+ """
picture(id: String!): Picture
relayFollowers(limit: Int = 10, page: Int = 1): PaginatedFollowerList
- relayFollowings(direction: String = "desc", limit: Int = 10, orderBy: String = "updated_at", page: Int = 1): PaginatedFollowerList
+ relayFollowings(
+ direction: String = "desc"
+ limit: Int = 10
+ orderBy: String = "updated_at"
+ page: Int = 1
+ ): PaginatedFollowerList
- """Get a report by id"""
+ """
+ Get a report by id
+ """
report(id: ID!): Report
- """Get all reports"""
+ """
+ Get all reports
+ """
reports(limit: Int = 10, page: Int = 1, status: ReportStatus = OPEN): [Report]
- """Get a resource"""
+ """
+ Get a resource
+ """
resource(id: ID, path: String, username: String): Resource
- """Reverse geocode coordinates"""
- reverseGeocode(latitude: Float!, locale: String = "en", longitude: Float!, zoom: Int = 15): [Address]
+ """
+ Reverse geocode coordinates
+ """
+ reverseGeocode(
+ latitude: Float!
+ locale: String = "en"
+ longitude: Float!
+ zoom: Int = 15
+ ): [Address]
- """Search for an address"""
- searchAddress(limit: Int = 10, locale: String = "en", page: Int = 1, query: String!): [Address]
+ """
+ Search for an address
+ """
+ searchAddress(
+ limit: Int = 10
+ locale: String = "en"
+ page: Int = 1
+ query: String!
+ ): [Address]
- """Search events"""
+ """
+ Search events
+ """
searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
- """Search groups"""
+ """
+ Search groups
+ """
searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups
- """Search persons"""
+ """
+ Search persons
+ """
searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons
- """Get the list of tags"""
+ """
+ Get the list of tags
+ """
tags(limit: Int = 10, page: Int = 1): [Tag]!
- """Get replies for thread"""
+ """
+ Get replies for thread
+ """
thread(id: ID): [Comment]
- """Get a todo"""
+ """
+ Get a todo
+ """
todo(id: ID!): Todo
- """Get a todo list"""
+ """
+ Get a todo list
+ """
todoList(id: ID!): TodoList
- """Get an user"""
+ """
+ Get an user
+ """
user(id: ID!): User
- """List instance users"""
- users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users
+ """
+ List instance users
+ """
+ users(
+ direction: SortDirection = DESC
+ limit: Int = 10
+ page: Int = 1
+ sort: SortableUserField = ID
+ ): Users
}
type RootSubscriptionType {
eventPersonParticipationChanged(personId: ID!): Person
}
-"""The list of possible options for the event's status"""
+"""
+The list of possible options for the event's status
+"""
enum SortableUserField {
ID
}
-"""Available sort directions"""
+"""
+Available sort directions
+"""
enum SortDirection {
ASC
DESC
}
-"""A tag"""
+"""
+A tag
+"""
type Tag {
- """The tag's ID"""
+ """
+ The tag's ID
+ """
id: ID
- """Related tags to this tag"""
+ """
+ Related tags to this tag
+ """
related: [Tag]
- """The tags's slug"""
+ """
+ The tags's slug
+ """
slug: String
- """The tag's title"""
+ """
+ The tag's title
+ """
title: String
}
@@ -1704,96 +2624,158 @@ type Tiles {
endpoint: String
}
-"""A todo"""
+"""
+A todo
+"""
type Todo {
- """The todos's assigned person"""
+ """
+ The todos's assigned person
+ """
assignedTo: Actor
- """The todo's creator"""
+ """
+ The todo's creator
+ """
creator: Actor
- """The todo's due date"""
+ """
+ The todo's due date
+ """
dueDate: DateTime
- """The todo's ID"""
+ """
+ The todo's ID
+ """
id: ID
- """The todo's status"""
+ """
+ The todo's status
+ """
status: Boolean
- """The todo's title"""
+ """
+ The todo's title
+ """
title: String
- """The todo list this todo is attached to"""
+ """
+ The todo list this todo is attached to
+ """
todoList: TodoList
}
-"""A todo list"""
+"""
+A todo list
+"""
type TodoList {
- """The actor that owns this todo list"""
+ """
+ The actor that owns this todo list
+ """
actor: Actor
- """The todo list's ID"""
+ """
+ The todo list's ID
+ """
id: ID
- """The todo list's title"""
+ """
+ The todo list's title
+ """
title: String
- """The todo-list's todos"""
+ """
+ The todo-list's todos
+ """
todos: PaginatedTodoList
}
"""
Represents an uploaded file.
-
"""
scalar Upload
-"""A local user of Mobilizon"""
+"""
+A local user of Mobilizon
+"""
type User {
- """The datetime the last activation/confirmation token was sent"""
+ """
+ The datetime the last activation/confirmation token was sent
+ """
confirmationSentAt: DateTime
- """The account activation/confirmation token"""
+ """
+ The account activation/confirmation token
+ """
confirmationToken: String
- """The datetime when the user was confirmed/activated"""
+ """
+ The datetime when the user was confirmed/activated
+ """
confirmedAt: DateTime
- """The user's default actor"""
+ """
+ The user's default actor
+ """
defaultActor: Person
- """The list of draft events this user has created"""
+ """
+ The list of draft events this user has created
+ """
drafts(limit: Int = 10, page: Int = 1): [Event]
- """The user's email"""
+ """
+ The user's email
+ """
email: String!
- """A list of the feed tokens for this user"""
+ """
+ A list of the feed tokens for this user
+ """
feedTokens: [FeedToken]
- """The user's ID"""
+ """
+ The user's ID
+ """
id: ID!
- """The user's locale"""
+ """
+ The user's locale
+ """
locale: String
- """The list of memberships for this user"""
+ """
+ The list of memberships for this user
+ """
memberships(limit: Int = 10, page: Int = 1): PaginatedMemberList
- """The list of participations this user has"""
- participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant]
+ """
+ The list of participations this user has
+ """
+ participations(
+ afterDatetime: DateTime
+ beforeDatetime: DateTime
+ limit: Int = 10
+ page: Int = 1
+ ): [Participant]
- """The user's list of profiles (identities)"""
+ """
+ The user's list of profiles (identities)
+ """
profiles: [Person]!
- """The datetime last reset password email was sent"""
+ """
+ The datetime last reset password email was sent
+ """
resetPasswordSentAt: DateTime
- """The token sent when requesting password token"""
+ """
+ The token sent when requesting password token
+ """
resetPasswordToken: String
- """The role for the user"""
+ """
+ The role for the user
+ """
role: UserRole
}
@@ -1803,12 +2785,18 @@ enum UserRole {
USER
}
-"""Users list"""
+"""
+Users list
+"""
type Users {
- """User elements"""
+ """
+ User elements
+ """
elements: [User]!
- """Total elements"""
+ """
+ Total elements
+ """
total: Int!
}
diff --git a/test/federation/activity_pub/activity_pub_test.exs b/test/federation/activity_pub/activity_pub_test.exs
index 6addf7a8..cb8bfda3 100644
--- a/test/federation/activity_pub/activity_pub_test.exs
+++ b/test/federation/activity_pub/activity_pub_test.exs
@@ -8,9 +8,10 @@ defmodule Mobilizon.Federation.ActivityPubTest do
use Mobilizon.DataCase
import Mock
+ import Mox
import Mobilizon.Factory
- alias Mobilizon.{Actors, Conversations, Events}
+ alias Mobilizon.{Actors, Discussions, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
@@ -18,13 +19,10 @@ defmodule Mobilizon.Federation.ActivityPubTest do
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Utils
alias Mobilizon.Federation.HTTPSignatures.Signature
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
@activity_pub_public_audience "https://www.w3.org/ns/activitystreams#Public"
- setup_all do
- HTTPoison.start()
- end
-
describe "setting HTTP signature" do
test "set http signature header" do
actor = insert(:actor)
@@ -140,40 +138,75 @@ defmodule Mobilizon.Federation.ActivityPubTest do
describe "fetching an" do
test "object by url" do
- use_cassette "activity_pub/fetch_framapiaf_framasoft_status" do
- {:ok, object} =
- ActivityPub.fetch_object_from_url(
- "https://framapiaf.org/users/Framasoft/statuses/102093631881522097"
- )
+ url = "https://framapiaf.org/users/Framasoft/statuses/102093631881522097"
- {:ok, object_again} =
- ActivityPub.fetch_object_from_url(
- "https://framapiaf.org/users/Framasoft/statuses/102093631881522097"
- )
+ data =
+ File.read!("test/fixtures/mastodon-status-2.json")
+ |> Jason.decode!()
- assert object.id == object_again.id
- end
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: ^url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ {:ok, object} = ActivityPub.fetch_object_from_url(url)
+
+ {:ok, object_again} = ActivityPub.fetch_object_from_url(url)
+
+ assert object.id == object_again.id
end
test "object reply by url" do
- use_cassette "activity_pub/fetch_framasoft_framapiaf_reply" do
- {:ok, object} =
- ActivityPub.fetch_object_from_url("https://mamot.fr/@imacrea/102094441327423790")
+ url = "https://zoltasila.pl/objects/1c295713-8e3c-411e-9e62-57a7b9c9e514"
+ reply_to_url = "https://framapiaf.org/users/peertube/statuses/104584600044284729"
- assert object.in_reply_to_comment.url ==
- "https://framapiaf.org/users/Framasoft/statuses/102093632302210150"
- end
+ data =
+ File.read!("test/fixtures/mastodon-status-3.json")
+ |> Jason.decode!()
+
+ reply_to_data =
+ File.read!("test/fixtures/mastodon-status-4.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, 2, fn
+ %{method: :get, url: ^url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+
+ %{method: :get, url: ^reply_to_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: reply_to_data}}
+ end)
+
+ {:ok, object} = ActivityPub.fetch_object_from_url(url)
+
+ assert object.in_reply_to_comment.url == reply_to_url
end
test "object reply to a video by url" do
- use_cassette "activity_pub/fetch_reply_to_framatube" do
- {:ok, object} =
- ActivityPub.fetch_object_from_url(
- "https://diaspodon.fr/users/dada/statuses/100820008426311925"
- )
+ url = "https://diaspodon.fr/users/dada/statuses/100820008426311925"
+ origin_url = "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d"
- assert object.in_reply_to_comment == nil
- end
+ data =
+ File.read!("test/fixtures/mastodon-status-5.json")
+ |> Jason.decode!()
+
+ origin_data =
+ File.read!("test/fixtures/peertube-video.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, 2, fn
+ %{method: :get, url: ^url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+
+ %{method: :get, url: ^origin_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: origin_data}}
+ end)
+
+ {:ok, object} = ActivityPub.fetch_object_from_url(url)
+
+ assert object.in_reply_to_comment == nil
end
end
@@ -181,26 +214,28 @@ defmodule Mobilizon.Federation.ActivityPubTest do
test "it creates a delete activity and deletes the original event" do
event = insert(:event)
event = Events.get_public_event_by_url_with_preload!(event.url)
- {:ok, delete, _} = ActivityPub.delete(event)
+ {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == event.organizer_actor.url
- assert delete.data["object"] == event.url
+ assert delete.data["object"]["type"] == "Event"
+ assert delete.data["object"]["id"] == event.url
assert Events.get_event_by_url(event.url) == nil
end
test "it deletes the original event but only locally if needed" do
- with_mock Utils,
+ with_mock Utils, [:passthrough],
maybe_federate: fn _ -> :ok end,
lazy_put_activity_defaults: fn args -> args end do
event = insert(:event)
event = Events.get_public_event_by_url_with_preload!(event.url)
- {:ok, delete, _} = ActivityPub.delete(event, false)
+ {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor, false)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == event.organizer_actor.url
- assert delete.data["object"] == event.url
+ assert delete.data["object"]["type"] == "Event"
+ assert delete.data["object"]["id"] == event.url
assert delete.local == false
assert Events.get_event_by_url(event.url) == nil
@@ -211,15 +246,16 @@ defmodule Mobilizon.Federation.ActivityPubTest do
test "it creates a delete activity and deletes the original comment" do
comment = insert(:comment)
- comment = Conversations.get_comment_from_url_with_preload!(comment.url)
- assert is_nil(Conversations.get_comment_from_url(comment.url).deleted_at)
- {:ok, delete, _} = ActivityPub.delete(comment)
+ comment = Discussions.get_comment_from_url_with_preload!(comment.url)
+ assert is_nil(Discussions.get_comment_from_url(comment.url).deleted_at)
+ {:ok, delete, _} = ActivityPub.delete(comment, comment.actor)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == comment.actor.url
- assert delete.data["object"] == comment.url
+ assert delete.data["object"]["type"] == "Note"
+ assert delete.data["object"]["id"] == comment.url
- refute is_nil(Conversations.get_comment_from_url(comment.url).deleted_at)
+ refute is_nil(Discussions.get_comment_from_url(comment.url).deleted_at)
end
end
@@ -230,7 +266,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
actor = insert(:actor)
actor_data = %{summary: @updated_actor_summary}
- {:ok, update, _} = ActivityPub.update(:actor, actor, actor_data, false)
+ {:ok, update, _} = ActivityPub.update(actor, actor_data, false)
assert update.data["actor"] == actor.url
assert update.data["to"] == [@activity_pub_public_audience]
@@ -246,7 +282,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
event = insert(:event, organizer_actor: actor)
event_data = %{begins_on: @updated_start_time}
- {:ok, update, _} = ActivityPub.update(:event, event, event_data)
+ {:ok, update, _} = ActivityPub.update(event, event_data)
assert update.data["actor"] == actor.url
assert update.data["to"] == [@activity_pub_public_audience]
@@ -272,8 +308,8 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert create_data.local
assert create_data.data["object"]["id"] == todo_list_url
assert create_data.data["object"]["type"] == "TodoList"
- assert create_data.data["object"]["title"] == @todo_list_title
- assert create_data.data["to"] == [group.url]
+ assert create_data.data["object"]["name"] == @todo_list_title
+ assert create_data.data["to"] == [group.members_url]
assert create_data.data["actor"] == actor.url
assert_called(Utils.maybe_federate(create_data))
@@ -301,7 +337,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert create_data.data["object"]["id"] == todo_url
assert create_data.data["object"]["type"] == "Todo"
assert create_data.data["object"]["name"] == @todo_title
- assert create_data.data["to"] == [todo_list.actor.url]
+ assert create_data.data["to"] == [todo_list.actor.members_url]
assert create_data.data["actor"] == actor.url
assert_called(Utils.maybe_federate(create_data))
@@ -341,7 +377,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert create_data.data["object"]["url"] == @resource_url
- assert create_data.data["to"] == [group.url]
+ assert create_data.data["to"] == [group.members_url]
assert create_data.data["actor"] == actor.url
assert create_data.data["attributedTo"] == [actor.url]
@@ -372,7 +408,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert create_data.data["object"]["id"] == url
assert create_data.data["object"]["type"] == "ResourceCollection"
assert create_data.data["object"]["name"] == @folder_title
- assert create_data.data["to"] == [group.url]
+ assert create_data.data["to"] == [group.members_url]
assert create_data.data["actor"] == actor.url
assert create_data.data["attributedTo"] == [actor.url]
@@ -411,7 +447,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert create_data.data["object"]["url"] == @resource_url
- assert create_data.data["to"] == [group.url]
+ assert create_data.data["to"] == [group.members_url]
assert create_data.data["actor"] == actor.url
assert create_data.data["attributedTo"] == [actor.url]
@@ -437,7 +473,6 @@ defmodule Mobilizon.Federation.ActivityPubTest do
{:ok, update_data, %Resource{url: url}} =
ActivityPub.update(
- :resource,
resource,
%{
title: @updated_resource_title
@@ -453,7 +488,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert update_data.data["object"]["url"] == @resource_url
- assert update_data.data["to"] == [group.url]
+ assert update_data.data["to"] == [group.members_url]
assert update_data.data["actor"] == actor.url
assert update_data.data["attributedTo"] == [actor.url]
@@ -496,7 +531,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
assert update_data.data["object"]["url"] == @resource_url
- assert update_data.data["to"] == [group.url]
+ assert update_data.data["to"] == [group.members_url]
assert update_data.data["actor"] == actor.url
assert update_data.data["origin"] == nil
assert update_data.data["target"] == parent_url
@@ -524,19 +559,23 @@ defmodule Mobilizon.Federation.ActivityPubTest do
{:ok, update_data, %Resource{url: url}} =
ActivityPub.delete(
resource,
+ actor,
true
)
assert update_data.local
assert update_data.data["type"] == "Delete"
- assert update_data.data["object"] == url
- assert update_data.data["to"] == [group.url]
- # TODO : Add actor parameter to ActivityPub.delete/2
- # assert update_data.data["actor"] == actor.url
- # assert update_data.data["attributedTo"] == [actor.url]
+ assert update_data.data["object"]["type"] == "Document"
+ assert update_data.data["object"]["id"] == url
+ assert update_data.data["to"] == [group.members_url]
+ assert update_data.data["actor"] == actor.url
+ assert update_data.data["attributedTo"] == [group.url]
assert_called(Utils.maybe_federate(update_data))
end
end
end
+
+ describe "announce" do
+ end
end
diff --git a/test/federation/activity_pub/refresher_test.exs b/test/federation/activity_pub/refresher_test.exs
index 91339825..f164f5fd 100644
--- a/test/federation/activity_pub/refresher_test.exs
+++ b/test/federation/activity_pub/refresher_test.exs
@@ -2,37 +2,35 @@ defmodule Mobilizon.Federation.ActivityPub.RefresherTest do
use Mobilizon.DataCase
alias Mobilizon.Actors.{Actor, Member}
- alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Refresher
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
alias Mobilizon.Web.ActivityPub.ActorView
import Mobilizon.Factory
- import Mock
+ import Mox
- test "refreshes a members collection" do
- %Actor{members_url: members_url, url: group_url} = group = insert(:group)
- %Actor{url: actor_url} = actor = insert(:actor)
- %Member{} = insert(:member, parent: group, actor: actor, role: :member)
+ describe "refreshes a" do
+ setup :verify_on_exit!
- data =
- ActorView.render("members.json", %{group: group, actor_applicant: actor}) |> Jason.encode!()
+ test "members collection" do
+ %Actor{members_url: members_url} =
+ group =
+ insert(:group,
+ url: "https://remoteinstance.tld/@group",
+ members_url: "https://remoteinstance.tld/@group/members",
+ domain: "remoteinstance.tld"
+ )
+
+ %Actor{} = actor = insert(:actor)
+ %Member{} = insert(:member, parent: group, actor: actor, role: :member)
+
+ data = ActorView.render("members.json", %{actor: group, actor_applicant: actor})
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: ^members_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
- with_mocks([
- {HTTPoison, [],
- [
- get!: fn ^members_url, _headers, _options ->
- %HTTPoison.Response{status_code: 200, body: data}
- end
- ]},
- {ActivityPub, [],
- [
- get_or_fetch_actor_by_url: fn url ->
- case url do
- ^actor_url -> {:ok, actor}
- ^group_url -> {:ok, group}
- end
- end
- ]}
- ]) do
assert :ok == Refresher.fetch_collection(group.members_url, actor)
end
end
diff --git a/test/federation/activity_pub/transmogrifier/announces_test.exs b/test/federation/activity_pub/transmogrifier/announces_test.exs
new file mode 100644
index 00000000..68c1eead
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/announces_test.exs
@@ -0,0 +1,173 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.AnnouncesTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ import Mox
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions
+ alias Mobilizon.Discussions.{Comment, Discussion}
+ alias Mobilizon.Federation.ActivityPub.Transmogrifier
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
+ alias Mobilizon.Tombstone
+
+ @comment_text "my comment"
+
+ describe "incoming announces for discussion creation" do
+ setup :verify_on_exit!
+
+ test "by group member works" do
+ actor = insert(:actor)
+ group = insert(:group)
+ insert(:member, parent: group, actor: actor, role: :member)
+
+ %Comment{url: comment_url} =
+ comment = build(:comment, actor: actor, attributed_to: group, event: nil)
+
+ comment_data = Convertible.model_to_as(comment)
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: ^comment_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: comment_data}}
+ end)
+
+ data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Jason.decode!()
+ |> Map.put("actor", group.url)
+ |> Map.put("object", comment.url)
+
+ {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url}} =
+ Transmogrifier.handle_incoming(data)
+
+ assert actor_url == comment.actor.url
+
+ assert comment_url == comment.url
+ end
+ end
+
+ describe "handle incoming announces for discussion updates" do
+ setup :verify_on_exit!
+
+ @updated_title "Updated title"
+
+ test "by group member works" do
+ actor =
+ insert(:actor,
+ domain: "otherremoteinstance.tld",
+ url: "http://otherremoteinstance.tld/@somemember"
+ )
+
+ group =
+ insert(:group,
+ url: "http://remoteinstance.tld/@mygroup",
+ domain: "remoteinstance.tld",
+ members_url: "http://remoteinstance.tld/@mygroup/members"
+ )
+
+ insert(:member, parent: group, actor: actor, role: :member)
+
+ %Comment{url: _comment_url} =
+ comment =
+ insert(:comment,
+ actor: actor,
+ attributed_to: group,
+ text: @comment_text,
+ url: "http://otherremoteinstance.tld/@somemember/uuid"
+ )
+
+ %Discussion{url: discussion_url} =
+ discussion =
+ insert(:discussion,
+ last_comment: comment,
+ comments: [comment],
+ creator: actor,
+ actor: group,
+ url: "http://otherremoteinstance.tld/@mygroup/c/talk-of-something-sh0rt-uu1d"
+ )
+
+ discussion_updated = Map.put(discussion, :title, @updated_title)
+
+ discussion_updated_data = Convertible.model_to_as(discussion_updated)
+
+ Mock
+ |> expect(:call, fn
+ %{url: ^discussion_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: discussion_updated_data}}
+ end)
+
+ data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Jason.decode!()
+ |> Map.put("actor", group.url)
+ |> Map.put("object", discussion_url)
+
+ assert {:ok, _, %Discussion{title: title}} = Transmogrifier.handle_incoming(data)
+ assert title == @updated_title
+ end
+ end
+
+ describe "handle incoming announces for discussion deletion" do
+ setup :verify_on_exit!
+
+ test "by group member works" do
+ actor =
+ insert(:actor,
+ url: "http://otherremoteinstance.tld/@somemember",
+ domain: "otherremoteinstance.tld"
+ )
+
+ group =
+ insert(:group,
+ url: "http://remoteinstance.tld/@mygroup",
+ domain: "remoteinstance.tld",
+ members_url: "http://remoteinstance.tld/@mygroup/members"
+ )
+
+ insert(:member, parent: group, actor: actor, role: :member)
+
+ %Comment{url: comment_url} =
+ comment =
+ insert(:comment,
+ actor: actor,
+ attributed_to: group,
+ text: @comment_text,
+ url: "http://otherremoteinstance.tld/comment/uuid"
+ )
+
+ tombstone = build(:tombstone, uri: comment.url, actor: actor)
+ tombstone_data = Convertible.model_to_as(tombstone)
+
+ Mock
+ |> expect(:call, fn
+ %{url: ^comment_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: tombstone_data}}
+ end)
+
+ data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Jason.decode!()
+ |> Map.put("actor", group.url)
+ |> Map.put("object", comment.url)
+
+ %Comment{deleted_at: deleted_at, text: comment_text} =
+ Discussions.get_comment_from_url(comment.url)
+
+ assert is_nil(deleted_at)
+ assert comment_text == @comment_text
+
+ {:ok, _, %Comment{deleted_at: deleted_at, text: comment_text}} =
+ Transmogrifier.handle_incoming(data)
+
+ refute is_nil(deleted_at)
+ refute comment_text == @comment_text
+
+ %Tombstone{actor_id: _actor_id, uri: tombstone_uri} = Tombstone.find_tombstone(comment_url)
+
+ # assert actor_id == comment.actor.id
+
+ assert tombstone_uri == comment.url
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/comments_test.exs b/test/federation/activity_pub/transmogrifier/comments_test.exs
new file mode 100644
index 00000000..897d8eab
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/comments_test.exs
@@ -0,0 +1,155 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.CommentsTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ import Mox
+ import ExUnit.CaptureLog
+ alias Mobilizon.Actors
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions
+ alias Mobilizon.Discussions.Comment
+ alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
+ alias Mobilizon.Federation.ActivityStream.Convertible
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
+
+ describe "handle incoming comments" do
+ setup :verify_on_exit!
+
+ test "it ignores an incoming comment if we already have it" do
+ comment = insert(:comment)
+ comment = Repo.preload(comment, [:attributed_to])
+
+ activity = %{
+ "type" => "Create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "actor" => comment.actor.url,
+ "object" => Convertible.model_to_as(comment)
+ }
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity["object"])
+
+ assert {:ok, nil, _} = Transmogrifier.handle_incoming(data)
+ end
+
+ test "it fetches replied-to activities if we don't have them" do
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Jason.decode!()
+
+ reply_to_url = "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94"
+
+ object =
+ data["object"]
+ |> Map.put("inReplyTo", reply_to_url)
+
+ data =
+ data
+ |> Map.put("object", object)
+
+ reply_to_data =
+ File.read!("test/fixtures/pleroma-comment-object.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: ^reply_to_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: reply_to_data}}
+ end)
+
+ {:ok, returned_activity, _} = Transmogrifier.handle_incoming(data)
+
+ %Comment{} =
+ origin_comment =
+ Discussions.get_comment_from_url(
+ "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94"
+ )
+
+ assert returned_activity.data["object"]["inReplyTo"] ==
+ "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94"
+
+ assert returned_activity.data["object"]["inReplyTo"] == origin_comment.url
+ end
+
+ @url_404 "https://404.site/whatever"
+ test "it does not crash if the object in inReplyTo can't be fetched" do
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Jason.decode!()
+
+ object =
+ data["object"]
+ |> Map.put("inReplyTo", @url_404)
+
+ data =
+ data
+ |> Map.put("object", object)
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://404.site/whatever"}, _opts ->
+ {:ok, %Tesla.Env{status: 404, body: "Not found"}}
+ end)
+
+ assert capture_log([level: :warn], fn ->
+ {:ok, _returned_activity, _entity} = Transmogrifier.handle_incoming(data)
+ end) =~ "[warn] Parent object is something we don't handle"
+ end
+
+ test "it works for incoming notices" do
+ data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
+
+ {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+
+ assert data["id"] ==
+ "https://framapiaf.org/users/admin/statuses/99512778738411822/activity"
+
+ assert data["to"] == [
+ "https://www.w3.org/ns/activitystreams#Public",
+ "https://framapiaf.org/users/tcit"
+ ]
+
+ # assert data["cc"] == [
+ # "https://framapiaf.org/users/admin/followers",
+ # "http://mobilizon.com/@tcit"
+ # ]
+
+ assert data["actor"] == "https://framapiaf.org/users/admin"
+
+ object = data["object"]
+ assert object["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822"
+
+ assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
+
+ # assert object["cc"] == [
+ # "https://framapiaf.org/users/admin/followers",
+ # "http://localtesting.pleroma.lol/users/lain"
+ # ]
+
+ assert object["actor"] == "https://framapiaf.org/users/admin"
+ assert object["attributedTo"] == "https://framapiaf.org/users/admin"
+
+ {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"])
+ end
+
+ test "it works for incoming notices with hashtags" do
+ data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
+
+ {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+ assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org"
+ assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo"
+ end
+
+ test "it works for incoming notices with url not being a string (prismo)" do
+ data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!()
+
+ assert {:error, :not_supported} == Transmogrifier.handle_incoming(data)
+ # Pages without groups are not supported
+ # {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ # assert data["object"]["url"] == "https://prismo.news/posts/83"
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/follow_test.exs b/test/federation/activity_pub/transmogrifier/follow_test.exs
new file mode 100644
index 00000000..5c15bd14
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/follow_test.exs
@@ -0,0 +1,122 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ alias Mobilizon.Actors
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
+
+ describe "handle incoming follow accept activities" do
+ test "it works for incoming accepts which were pre-accepted" do
+ follower = insert(:actor)
+ followed = insert(:actor)
+
+ refute Actors.is_following(follower, followed)
+
+ {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
+ assert Actors.is_following(follower, followed)
+
+ accept_data =
+ File.read!("test/fixtures/mastodon-accept-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", followed.url)
+
+ object =
+ accept_data["object"]
+ |> Map.put("actor", follower.url)
+ |> Map.put("id", follow_activity.data["id"])
+
+ accept_data = Map.put(accept_data, "object", object)
+
+ {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
+ refute activity.local
+
+ assert activity.data["object"]["id"] == follow_activity.data["id"]
+
+ {:ok, follower} = Actors.get_actor_by_url(follower.url)
+
+ assert Actors.is_following(follower, followed)
+ end
+
+ test "it works for incoming accepts which are referenced by IRI only" do
+ follower = insert(:actor)
+ followed = insert(:actor)
+
+ {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
+
+ accept_data =
+ File.read!("test/fixtures/mastodon-accept-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", followed.url)
+ |> Map.put("object", follow_activity.data["id"])
+
+ {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
+ assert activity.data["object"]["id"] == follow_activity.data["id"]
+ assert activity.data["object"]["id"] =~ "/follow/"
+ assert activity.data["id"] =~ "/accept/follow/"
+
+ {:ok, follower} = Actors.get_actor_by_url(follower.url)
+
+ assert Actors.is_following(follower, followed)
+ end
+
+ test "it fails for incoming accepts which cannot be correlated" do
+ follower = insert(:actor)
+ followed = insert(:actor)
+
+ accept_data =
+ File.read!("test/fixtures/mastodon-accept-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", followed.url)
+
+ accept_data =
+ Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
+
+ :error = Transmogrifier.handle_incoming(accept_data)
+
+ {:ok, follower} = Actors.get_actor_by_url(follower.url)
+
+ refute Actors.is_following(follower, followed)
+ end
+ end
+
+ describe "handle incoming follow reject activities" do
+ test "it fails for incoming rejects which cannot be correlated" do
+ follower = insert(:actor)
+ followed = insert(:actor)
+
+ accept_data =
+ File.read!("test/fixtures/mastodon-reject-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", followed.url)
+
+ accept_data =
+ Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
+
+ :error = Transmogrifier.handle_incoming(accept_data)
+
+ {:ok, follower} = Actors.get_actor_by_url(follower.url)
+
+ refute Actors.is_following(follower, followed)
+ end
+
+ test "it works for incoming rejects which are referenced by IRI only" do
+ follower = insert(:actor)
+ followed = insert(:actor)
+
+ {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
+
+ assert Actors.is_following(follower, followed)
+
+ reject_data =
+ File.read!("test/fixtures/mastodon-reject-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", followed.url)
+ |> Map.put("object", follow_activity.data["id"])
+
+ {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
+
+ refute Actors.is_following(follower, followed)
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/invite_test.exs b/test/federation/activity_pub/transmogrifier/invite_test.exs
new file mode 100644
index 00000000..9f64a535
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/invite_test.exs
@@ -0,0 +1,60 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.InviteTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ alias Mobilizon.Actors
+ alias Mobilizon.Actors.{Actor, Member}
+ alias Mobilizon.Federation.ActivityPub.Transmogrifier
+
+ describe "handle Invite activities on group" do
+ test "it accepts Invite activities" do
+ %Actor{url: group_url, id: group_id} = group = insert(:group)
+ %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor)
+
+ %Member{} =
+ _group_admin_member =
+ insert(:member, parent: group, actor: group_admin, role: :administrator)
+
+ %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor)
+
+ invite_data =
+ File.read!("test/fixtures/mobilizon-invite-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", group_admin_url)
+ |> Map.put("object", group_url)
+ |> Map.put("target", invitee_url)
+
+ assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data)
+ assert %Member{} = member = Actors.get_member_by_url(invite_data["id"])
+ assert member.actor.id == invitee_id
+ assert member.parent.id == group_id
+ assert member.role == :invited
+ assert member.invited_by_id == group_admin_id
+ end
+
+ test "it refuses Invite activities for " do
+ %Actor{url: group_url, id: group_id} = group = insert(:group)
+ %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor)
+
+ %Member{} =
+ _group_admin_member =
+ insert(:member, parent: group, actor: group_admin, role: :administrator)
+
+ %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor)
+
+ invite_data =
+ File.read!("test/fixtures/mobilizon-invite-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", group_admin_url)
+ |> Map.put("object", group_url)
+ |> Map.put("target", invitee_url)
+
+ assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data)
+ assert %Member{} = member = Actors.get_member_by_url(invite_data["id"])
+ assert member.actor.id == invitee_id
+ assert member.parent.id == group_id
+ assert member.role == :invited
+ assert member.invited_by_id == group_admin_id
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/join_test.exs b/test/federation/activity_pub/transmogrifier/join_test.exs
new file mode 100644
index 00000000..9bcbba32
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/join_test.exs
@@ -0,0 +1,104 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ import ExUnit.CaptureLog
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Events
+ alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.Transmogrifier
+
+ describe "handle incoming join activities" do
+ @join_message "I want to get in!"
+ test "it accepts Join activities" do
+ %Actor{url: organizer_url} = organizer = insert(:actor)
+ %Actor{url: participant_url} = _participant = insert(:actor)
+
+ %Event{url: event_url} = _event = insert(:event, organizer_actor: organizer)
+
+ join_data =
+ File.read!("test/fixtures/mobilizon-join-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", participant_url)
+ |> Map.put("object", event_url)
+ |> Map.put("participationMessage", @join_message)
+
+ assert {:ok, activity, %Participant{} = participant} =
+ Transmogrifier.handle_incoming(join_data)
+
+ assert participant.metadata.message == @join_message
+ assert participant.role == :participant
+
+ assert activity.data["type"] == "Accept"
+ assert activity.data["object"]["object"] == event_url
+ assert activity.data["object"]["id"] =~ "/join/event/"
+ assert activity.data["object"]["type"] =~ "Join"
+ assert activity.data["object"]["participationMessage"] == @join_message
+ assert activity.data["actor"] == organizer_url
+ assert activity.data["id"] =~ "/accept/join/"
+ end
+ end
+
+ describe "handle incoming accept join activities" do
+ test "it accepts Accept activities for Join activities" do
+ %Actor{url: organizer_url} = organizer = insert(:actor)
+ %Actor{} = participant_actor = insert(:actor)
+
+ %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
+
+ {:ok, join_activity, participation} =
+ ActivityPub.join(event, participant_actor, false, %{metadata: %{role: :not_approved}})
+
+ accept_data =
+ File.read!("test/fixtures/mastodon-accept-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", organizer_url)
+ |> Map.put("object", participation.url)
+
+ {:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
+ assert accept_activity.data["object"]["id"] == join_activity.data["id"]
+ assert accept_activity.data["object"]["id"] =~ "/join/"
+ assert accept_activity.data["id"] =~ "/accept/join/"
+
+ # We don't accept already accepted Accept activities
+ :error = Transmogrifier.handle_incoming(accept_data)
+ end
+ end
+
+ describe "handle incoming reject join activities" do
+ test "it accepts Reject activities for Join activities" do
+ %Actor{url: organizer_url} = organizer = insert(:actor)
+ %Actor{} = participant_actor = insert(:actor)
+
+ %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
+
+ {:ok, join_activity, participation} = ActivityPub.join(event, participant_actor)
+
+ reject_data =
+ File.read!("test/fixtures/mastodon-reject-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", organizer_url)
+ |> Map.put("object", participation.url)
+
+ {:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data)
+ assert reject_activity.data["object"]["id"] == join_activity.data["id"]
+ assert reject_activity.data["object"]["id"] =~ "/join/"
+ assert reject_activity.data["id"] =~ "/reject/join/"
+
+ # We don't accept already rejected Reject activities
+ assert capture_log([level: :warn], fn ->
+ assert :error == Transmogrifier.handle_incoming(reject_data)
+ end) =~
+ "Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{
+ join_activity.data["id"]
+ }\" wasn't found."
+
+ # Organiser is not present since we use factories directly
+ assert event.id
+ |> Events.list_participants_for_event()
+ |> Map.get(:elements)
+ |> Enum.map(& &1.role) == [:rejected]
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/leave_test.exs b/test/federation/activity_pub/transmogrifier/leave_test.exs
new file mode 100644
index 00000000..c72f5471
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/leave_test.exs
@@ -0,0 +1,60 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.LeaveTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Events
+ alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Federation.ActivityPub.Transmogrifier
+
+ describe "handle incoming leave activities on events" do
+ test "it accepts Leave activities" do
+ %Actor{url: _organizer_url} = organizer = insert(:actor)
+ %Actor{url: participant_url} = participant_actor = insert(:actor)
+
+ %Event{url: event_url} =
+ event = insert(:event, organizer_actor: organizer, join_options: :restricted)
+
+ organizer_participation =
+ %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
+
+ {:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor)
+
+ join_data =
+ File.read!("test/fixtures/mobilizon-leave-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", participant_url)
+ |> Map.put("object", event_url)
+
+ assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data)
+
+ assert activity.data["object"] == event_url
+ assert activity.data["actor"] == participant_url
+
+ # The only participant left is the organizer
+ assert event.id
+ |> Events.list_participants_for_event()
+ |> Map.get(:elements)
+ |> Enum.map(& &1.id) ==
+ [organizer_participation.id]
+ end
+
+ test "it refuses Leave activities when actor is the only organizer" do
+ %Actor{url: organizer_url} = organizer = insert(:actor)
+
+ %Event{url: event_url} =
+ event = insert(:event, organizer_actor: organizer, join_options: :restricted)
+
+ %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
+
+ join_data =
+ File.read!("test/fixtures/mobilizon-leave-activity.json")
+ |> Jason.decode!()
+ |> Map.put("actor", organizer_url)
+ |> Map.put("object", event_url)
+
+ assert :error = Transmogrifier.handle_incoming(join_data)
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier/undo_test.exs b/test/federation/activity_pub/transmogrifier/undo_test.exs
new file mode 100644
index 00000000..53ab9066
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/undo_test.exs
@@ -0,0 +1,85 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+ import Mox
+ alias Mobilizon.Actors
+ alias Mobilizon.Discussions.Comment
+ alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
+
+ describe "handle incoming undo activities" do
+ test "it works for incoming unannounces with an existing notice" do
+ comment = insert(:comment)
+
+ announce_data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Jason.decode!()
+ |> Map.put("object", comment.url)
+
+ actor_data =
+ File.read!("test/fixtures/mastodon-actor.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://framapiaf.org/users/Framasoft"}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: actor_data}}
+ end)
+
+ {:ok, _, %Comment{}} = Transmogrifier.handle_incoming(announce_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-announce.json")
+ |> Jason.decode!()
+ |> Map.put("object", announce_data)
+ |> Map.put("actor", announce_data["actor"])
+
+ {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Undo"
+ assert data["object"]["type"] == "Announce"
+ assert data["object"]["object"] == comment.url
+
+ assert data["object"]["id"] ==
+ "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity"
+ end
+
+ test "it works for incomming unfollows with an existing follow" do
+ actor = insert(:actor)
+
+ follow_data =
+ File.read!("test/fixtures/mastodon-follow-activity.json")
+ |> Jason.decode!()
+ |> Map.put("object", actor.url)
+
+ actor_data =
+ File.read!("test/fixtures/mastodon-actor.json")
+ |> Jason.decode!()
+ |> Map.put("id", "https://social.tcit.fr/users/tcit")
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://social.tcit.fr/users/tcit"}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: actor_data}}
+ end)
+
+ {:ok, %Activity{data: _, local: false}, _} = Transmogrifier.handle_incoming(follow_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-unfollow-activity.json")
+ |> Jason.decode!()
+ |> Map.put("object", follow_data)
+
+ {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Undo"
+ assert data["object"]["type"] == "Follow"
+ assert data["object"]["object"] == actor.url
+ assert data["actor"] == "https://social.tcit.fr/users/tcit"
+
+ {:ok, followed} = Actors.get_actor_by_url(data["actor"])
+ refute Actors.is_following(followed, actor)
+ end
+ end
+end
diff --git a/test/federation/activity_pub/transmogrifier_test.exs b/test/federation/activity_pub/transmogrifier_test.exs
index 1960c46b..3d4cdbfe 100644
--- a/test/federation/activity_pub/transmogrifier_test.exs
+++ b/test/federation/activity_pub/transmogrifier_test.exs
@@ -11,11 +11,12 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
import Mobilizon.Factory
import ExUnit.CaptureLog
import Mock
+ import Mox
- alias Mobilizon.{Actors, Conversations, Events}
- alias Mobilizon.Actors.{Actor, Member}
- alias Mobilizon.Conversations.Comment
- alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.{Actors, Discussions, Events}
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Discussions.Comment
+ alias Mobilizon.Events.Event
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
@@ -25,13 +26,10 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API
-
+ alias Mobilizon.Service.HTTP.ActivityPub.Mock
+ alias Mobilizon.Tombstone
alias Mobilizon.Web.Endpoint
- setup_all do
- HTTPoison.start()
- end
-
describe "handle incoming events" do
test "it works for incoming events" do
use_cassette "activity_pub/fetch_mobilizon_post_activity" do
@@ -67,7 +65,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
assert object["actor"] == "https://test.mobilizon.org/@Alicia"
assert object["location"]["name"] == "Locaux de Framasoft"
- assert object["attributedTo"] == "https://test.mobilizon.org/@Alicia"
+ # assert object["attributedTo"] == "https://test.mobilizon.org/@Alicia"
assert event.physical_address.street == "10 Rue Jangot"
@@ -79,172 +77,47 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
{:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"])
end
end
- end
- describe "handle incoming comments" do
- test "it ignores an incoming comment if we already have it" do
- comment = insert(:comment)
+ test "it works for incoming events for local groups" do
+ %Actor{url: group_url, id: group_id} = group = insert(:group)
- activity = %{
- "type" => "Create",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "actor" => comment.actor.url,
- "object" => Convertible.model_to_as(comment)
- }
-
- data =
- File.read!("test/fixtures/mastodon-post-activity.json")
- |> Jason.decode!()
- |> Map.put("object", activity["object"])
-
- assert {:ok, nil, _} = Transmogrifier.handle_incoming(data)
- end
-
- test "it fetches replied-to activities if we don't have them" do
- data =
- File.read!("test/fixtures/mastodon-post-activity.json")
- |> Jason.decode!()
-
- object =
- data["object"]
- |> Map.put("inReplyTo", "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94")
-
- data =
- data
- |> Map.put("object", object)
-
- {:ok, returned_activity, _} = Transmogrifier.handle_incoming(data)
-
- %Comment{} =
- origin_comment =
- Conversations.get_comment_from_url(
- "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94"
+ %Actor{url: actor_url, id: actor_id} =
+ actor =
+ insert(:actor,
+ domain: "test.mobilizon.org",
+ url: "https://test.mobilizon.org/@member",
+ preferred_username: "member"
)
- assert returned_activity.data["object"]["inReplyTo"] ==
- "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94"
+ with_mock ActivityPub, [:passthrough],
+ get_or_fetch_actor_by_url: fn url ->
+ case url do
+ ^group_url -> {:ok, group}
+ ^actor_url -> {:ok, actor}
+ end
+ end do
+ data = File.read!("test/fixtures/mobilizon-post-activity-group.json") |> Jason.decode!()
- assert returned_activity.data["object"]["inReplyTo"] == origin_comment.url
- end
+ object =
+ data["object"] |> Map.put("actor", actor_url) |> Map.put("attributedTo", group_url)
- test "it does not crash if the object in inReplyTo can't be fetched" do
- data =
- File.read!("test/fixtures/mastodon-post-activity.json")
- |> Poison.decode!()
+ data =
+ data
+ |> Map.put("actor", actor_url)
+ |> Map.put("attributedTo", group_url)
+ |> Map.put("object", object)
- object =
- data["object"]
- |> Map.put("inReplyTo", "https://404.site/whatever")
+ assert {:ok, %Activity{data: activity_data, local: false}, %Event{} = event} =
+ Transmogrifier.handle_incoming(data)
- data =
- data
- |> Map.put("object", object)
-
- assert capture_log([level: :warn], fn ->
- {:ok, _returned_activity, _entity} = Transmogrifier.handle_incoming(data)
- end) =~ "[warn] Parent object is something we don't handle"
- end
-
- test "it works for incoming notices" do
- use_cassette "activity_pub/mastodon_post_activity" do
- data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
-
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- assert data["id"] ==
- "https://framapiaf.org/users/admin/statuses/99512778738411822/activity"
-
- assert data["to"] == [
- "https://www.w3.org/ns/activitystreams#Public",
- "https://framapiaf.org/users/tcit"
- ]
-
- # assert data["cc"] == [
- # "https://framapiaf.org/users/admin/followers",
- # "http://mobilizon.com/@tcit"
- # ]
-
- assert data["actor"] == "https://framapiaf.org/users/admin"
-
- object = data["object"]
- assert object["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822"
-
- assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
-
- # assert object["cc"] == [
- # "https://framapiaf.org/users/admin/followers",
- # "http://localtesting.pleroma.lol/users/lain"
- # ]
-
- assert object["actor"] == "https://framapiaf.org/users/admin"
- assert object["attributedTo"] == "https://framapiaf.org/users/admin"
-
- {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"])
+ assert event.organizer_actor_id == actor_id
+ assert event.attributed_to_id == group_id
+ assert activity_data["actor"] == actor_url
+ assert activity_data["attributedTo"] == group_url
+ assert activity_data["object"]["actor"] == actor_url
+ assert activity_data["object"]["attributedTo"] == group_url
end
end
-
- test "it works for incoming notices with hashtags" do
- use_cassette "activity_pub/mastodon_activity_hashtag" do
- data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
-
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
- assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org"
- assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo"
- end
- end
-
- # test "it works for incoming notices with contentMap" do
- # data =
- # File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Jason.decode!()
-
- # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- # assert data["object"]["content"] ==
- # "
@lain
"
- # end
-
- # test "it works for incoming notices with to/cc not being an array (kroeg)" do
- # data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!()
-
- # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- # assert data["object"]["content"] ==
- # "
henlo from my Psion netBook
message sent from my Psion netBook
"
- # end
-
- # test "it works for incoming announces with actor being inlined (kroeg)" do
- # data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Jason.decode!()
-
- # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- # assert data["actor"] == "https://puckipedia.com/"
- # end
-
- # test "it works for incoming notices with tag not being an array (kroeg)" do
- # data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Jason.decode!()
-
- # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- # assert data["object"]["emoji"] == %{
- # "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
- # }
-
- # data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Jason.decode!()
-
- # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- # assert "test" in data["object"]["tag"]
- # end
-
- test "it works for incoming notices with url not being a string (prismo)" do
- data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!()
-
- assert {:error, :not_supported} == Transmogrifier.handle_incoming(data)
- # Pages are not supported
- # {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- # assert data["object"]["url"] == "https://prismo.news/posts/83"
- end
end
describe "handle incoming todo lists" do
@@ -463,7 +336,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => actor.url,
"target" => group.resources_url,
"object" => Convertible.model_to_as(resource)
@@ -491,7 +364,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => creator.url,
"target" => group.resources_url,
"object" => %{
@@ -534,7 +407,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => creator.url,
"target" => group.resources_url,
"object" => %{
@@ -586,7 +459,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => creator.url,
"target" => parent_resource.url,
"object" => %{
@@ -631,7 +504,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => creator.url,
"target" => group.resources_url,
"object" => %{
@@ -665,7 +538,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
activity = %{
"type" => "Add",
- "to" => [group.url],
+ "to" => [group.members_url],
"actor" => creator.url,
"target" => group.resources_url,
"object" => %{
@@ -787,43 +660,49 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
describe "handle incoming follow announces" do
test "it works for incoming announces" do
- use_cassette "activity_pub/mastodon_announce_activity" do
- data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
+ data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
+ status_data = File.read!("test/fixtures/mastodon-status.json") |> Jason.decode!()
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://framapiaf.org/users/peertube/statuses/104584600044284729"},
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: status_data}}
+ end)
- assert data["actor"] == "https://framapiaf.org/users/Framasoft"
- assert data["type"] == "Announce"
+ {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url}} =
+ Transmogrifier.handle_incoming(data)
- assert data["id"] ==
- "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
+ assert actor_url == "https://framapiaf.org/users/peertube"
- assert data["object"] ==
- "https://framapiaf.org/users/Framasoft/statuses/102501959686438400"
-
- assert %Comment{} = Conversations.get_comment_from_url(data["object"])
- end
+ assert comment_url ==
+ "https://framapiaf.org/users/peertube/statuses/104584600044284729"
end
test "it works for incoming announces with an existing activity" do
- use_cassette "activity_pub/mastodon_announce_existing_activity" do
- comment = insert(:comment)
+ %Comment{url: comment_url, actor: %Actor{url: actor_url} = actor} = insert(:comment)
- data =
- File.read!("test/fixtures/mastodon-announce.json")
- |> Jason.decode!()
- |> Map.put("object", comment.url)
+ actor_data =
+ File.read!("test/fixtures/mastodon-actor.json")
+ |> Jason.decode!()
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+ data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Jason.decode!()
+ |> Map.put("object", comment_url)
- assert data["actor"] == "https://framapiaf.org/users/Framasoft"
- assert data["type"] == "Announce"
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: actor_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: actor_data}}
+ end)
- assert data["id"] ==
- "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
+ {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url_2}} =
+ Transmogrifier.handle_incoming(data)
- assert data["object"] == comment.url
- end
+ assert actor_url == actor.url
+
+ assert comment_url == comment_url_2
end
end
@@ -926,12 +805,12 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|> Map.put("object", object)
|> Map.put("actor", actor_url)
- assert Conversations.get_comment_from_url(comment_url)
- assert is_nil(Conversations.get_comment_from_url(comment_url).deleted_at)
+ assert Discussions.get_comment_from_url(comment_url)
+ assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
{:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data)
- refute is_nil(Conversations.get_comment_from_url(comment_url).deleted_at)
+ refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
end
test "it fails for incoming deletes with spoofed origin" do
@@ -942,7 +821,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|> Jason.decode!()
|> Map.put("object", comment.url)
- {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(announce_data)
+ {:ok, _, _} = Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-delete.json")
@@ -958,9 +837,11 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
:error = Transmogrifier.handle_incoming(data)
- assert Conversations.get_comment_from_url(comment.url)
+ assert Discussions.get_comment_from_url(comment.url)
end
+ setup :set_mox_from_context
+
test "it works for incoming actor deletes" do
%Actor{url: url} = actor = insert(:actor, url: "https://framapiaf.org/users/admin")
%Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
@@ -971,7 +852,13 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
data =
File.read!("test/fixtures/mastodon-delete-user.json")
- |> Poison.decode!()
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
+ {:ok, %Tesla.Env{status: 410, body: "Gone"}}
+ end)
{:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
@@ -980,19 +867,31 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
assert {:error, :event_not_found} = Events.get_event(event1.id)
# Tombstone are cascade deleted, seems correct for now
# assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
- assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id)
+ assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
refute is_nil(deleted_at)
# assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
end
test "it fails for incoming actor deletes with spoofed origin" do
%{url: url} = insert(:actor)
+ deleted_actor_url = "https://framapiaf.org/users/admin"
data =
File.read!("test/fixtures/mastodon-delete-user.json")
- |> Poison.decode!()
+ |> Jason.decode!()
|> Map.put("actor", url)
+ deleted_actor_data =
+ File.read!("test/fixtures/mastodon-actor.json")
+ |> Jason.decode!()
+ |> Map.put("id", deleted_actor_url)
+
+ Mock
+ |> expect(:call, fn
+ %{url: ^deleted_actor_url}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: deleted_actor_data}}
+ end)
+
assert capture_log(fn ->
assert :error == Transmogrifier.handle_incoming(data)
end) =~ "Object origin check failed"
@@ -1001,62 +900,29 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
end
end
- describe "handle incoming undo activities" do
- test "it works for incoming unannounces with an existing notice" do
- use_cassette "activity_pub/mastodon_unannounce_activity" do
- comment = insert(:comment)
+ describe "handle tombstones" do
+ setup :verify_on_exit!
- announce_data =
- File.read!("test/fixtures/mastodon-announce.json")
- |> Jason.decode!()
- |> Map.put("object", comment.url)
+ # This is a hack to handle fetching tombstones
+ test "works for incoming tombstone creations" do
+ %Comment{url: comment_url} = comment = insert(:comment, local: false)
+ tombstone = build(:tombstone, uri: comment_url)
+ data = Convertible.model_to_as(tombstone)
- {:ok, %Activity{data: announce_data, local: false}, _} =
- Transmogrifier.handle_incoming(announce_data)
+ activity = %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ "actor" => data["actor"],
+ "attributedTo" => data["attributedTo"],
+ "object" => data
+ }
- data =
- File.read!("test/fixtures/mastodon-undo-announce.json")
- |> Jason.decode!()
- |> Map.put("object", announce_data)
- |> Map.put("actor", announce_data["actor"])
-
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- assert data["type"] == "Undo"
- assert data["object"]["type"] == "Announce"
- assert data["object"]["object"] == comment.url
-
- assert data["object"]["id"] ==
- "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
- end
- end
-
- test "it works for incomming unfollows with an existing follow" do
- use_cassette "activity_pub/unfollow_existing_follow_activity" do
- actor = insert(:actor)
-
- follow_data =
- File.read!("test/fixtures/mastodon-follow-activity.json")
- |> Jason.decode!()
- |> Map.put("object", actor.url)
-
- {:ok, %Activity{data: _, local: false}, _} = Transmogrifier.handle_incoming(follow_data)
-
- data =
- File.read!("test/fixtures/mastodon-unfollow-activity.json")
- |> Jason.decode!()
- |> Map.put("object", follow_data)
-
- {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-
- assert data["type"] == "Undo"
- assert data["object"]["type"] == "Follow"
- assert data["object"]["object"] == actor.url
- assert data["actor"] == "https://social.tcit.fr/users/tcit"
-
- {:ok, followed} = Actors.get_actor_by_url(data["actor"])
- refute Actors.is_following(followed, actor)
- end
+ {:ok, _activity, %Comment{url: comment_url}} = Transmogrifier.handle_incoming(activity)
+ assert comment_url == comment.url
+ assert %Comment{} = comment = Discussions.get_comment_from_url(comment_url)
+ assert %Tombstone{} = Tombstone.find_tombstone(comment_url)
+ refute is_nil(comment.deleted_at)
end
end
@@ -1136,120 +1002,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
# refute User.blocks?(blocker, user)
# end
- describe "handle incoming follow accept activities" do
- test "it works for incoming accepts which were pre-accepted" do
- follower = insert(:actor)
- followed = insert(:actor)
-
- refute Actors.is_following(follower, followed)
-
- {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
- assert Actors.is_following(follower, followed)
-
- accept_data =
- File.read!("test/fixtures/mastodon-accept-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", followed.url)
-
- object =
- accept_data["object"]
- |> Map.put("actor", follower.url)
- |> Map.put("id", follow_activity.data["id"])
-
- accept_data = Map.put(accept_data, "object", object)
-
- {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
- refute activity.local
-
- assert activity.data["object"]["id"] == follow_activity.data["id"]
-
- {:ok, follower} = Actors.get_actor_by_url(follower.url)
-
- assert Actors.is_following(follower, followed)
- end
-
- test "it works for incoming accepts which are referenced by IRI only" do
- follower = insert(:actor)
- followed = insert(:actor)
-
- {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
-
- accept_data =
- File.read!("test/fixtures/mastodon-accept-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", followed.url)
- |> Map.put("object", follow_activity.data["id"])
-
- {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
- assert activity.data["object"]["id"] == follow_activity.data["id"]
- assert activity.data["object"]["id"] =~ "/follow/"
- assert activity.data["id"] =~ "/accept/follow/"
-
- {:ok, follower} = Actors.get_actor_by_url(follower.url)
-
- assert Actors.is_following(follower, followed)
- end
-
- test "it fails for incoming accepts which cannot be correlated" do
- follower = insert(:actor)
- followed = insert(:actor)
-
- accept_data =
- File.read!("test/fixtures/mastodon-accept-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", followed.url)
-
- accept_data =
- Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
-
- :error = Transmogrifier.handle_incoming(accept_data)
-
- {:ok, follower} = Actors.get_actor_by_url(follower.url)
-
- refute Actors.is_following(follower, followed)
- end
- end
-
- describe "handle incoming follow reject activities" do
- test "it fails for incoming rejects which cannot be correlated" do
- follower = insert(:actor)
- followed = insert(:actor)
-
- accept_data =
- File.read!("test/fixtures/mastodon-reject-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", followed.url)
-
- accept_data =
- Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
-
- :error = Transmogrifier.handle_incoming(accept_data)
-
- {:ok, follower} = Actors.get_actor_by_url(follower.url)
-
- refute Actors.is_following(follower, followed)
- end
-
- test "it works for incoming rejects which are referenced by IRI only" do
- follower = insert(:actor)
- followed = insert(:actor)
-
- {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
-
- assert Actors.is_following(follower, followed)
-
- reject_data =
- File.read!("test/fixtures/mastodon-reject-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", followed.url)
- |> Map.put("object", follow_activity.data["id"])
-
- {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
-
- refute Actors.is_following(follower, followed)
- end
- end
-
describe "handle incoming flag activities" do
test "it accepts Flag activities" do
%Actor{url: reporter_url} = Relay.get_actor()
@@ -1276,201 +1028,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
end
end
- describe "handle incoming join activities" do
- @join_message "I want to get in!"
- test "it accepts Join activities" do
- %Actor{url: organizer_url} = organizer = insert(:actor)
- %Actor{url: participant_url} = _participant = insert(:actor)
-
- %Event{url: event_url} = _event = insert(:event, organizer_actor: organizer)
-
- join_data =
- File.read!("test/fixtures/mobilizon-join-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", participant_url)
- |> Map.put("object", event_url)
- |> Map.put("participationMessage", @join_message)
-
- assert {:ok, activity, %Participant{} = participant} =
- Transmogrifier.handle_incoming(join_data)
-
- assert participant.metadata.message == @join_message
- assert participant.role == :participant
-
- assert activity.data["type"] == "Accept"
- assert activity.data["object"]["object"] == event_url
- assert activity.data["object"]["id"] =~ "/join/event/"
- assert activity.data["object"]["type"] =~ "Join"
- assert activity.data["object"]["participationMessage"] == @join_message
- assert activity.data["actor"] == organizer_url
- assert activity.data["id"] =~ "/accept/join/"
- end
- end
-
- describe "handle incoming accept join activities" do
- test "it accepts Accept activities for Join activities" do
- %Actor{url: organizer_url} = organizer = insert(:actor)
- %Actor{} = participant_actor = insert(:actor)
-
- %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
-
- {:ok, join_activity, participation} =
- ActivityPub.join(event, participant_actor, false, %{metadata: %{role: :not_approved}})
-
- accept_data =
- File.read!("test/fixtures/mastodon-accept-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", organizer_url)
- |> Map.put("object", participation.url)
-
- {:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
- assert accept_activity.data["object"]["id"] == join_activity.data["id"]
- assert accept_activity.data["object"]["id"] =~ "/join/"
- assert accept_activity.data["id"] =~ "/accept/join/"
-
- # We don't accept already accepted Accept activities
- :error = Transmogrifier.handle_incoming(accept_data)
- end
- end
-
- describe "handle incoming reject join activities" do
- test "it accepts Reject activities for Join activities" do
- %Actor{url: organizer_url} = organizer = insert(:actor)
- %Actor{} = participant_actor = insert(:actor)
-
- %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
-
- {:ok, join_activity, participation} = ActivityPub.join(event, participant_actor)
-
- reject_data =
- File.read!("test/fixtures/mastodon-reject-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", organizer_url)
- |> Map.put("object", participation.url)
-
- {:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data)
- assert reject_activity.data["object"]["id"] == join_activity.data["id"]
- assert reject_activity.data["object"]["id"] =~ "/join/"
- assert reject_activity.data["id"] =~ "/reject/join/"
-
- # We don't accept already rejected Reject activities
- assert capture_log([level: :warn], fn ->
- assert :error == Transmogrifier.handle_incoming(reject_data)
- end) =~
- "Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{
- join_activity.data["id"]
- }\" wasn't found."
-
- # Organiser is not present since we use factories directly
- assert event.id
- |> Events.list_participants_for_event()
- |> Map.get(:elements)
- |> Enum.map(& &1.role) == [:rejected]
- end
- end
-
- describe "handle incoming leave activities on events" do
- test "it accepts Leave activities" do
- %Actor{url: _organizer_url} = organizer = insert(:actor)
- %Actor{url: participant_url} = participant_actor = insert(:actor)
-
- %Event{url: event_url} =
- event = insert(:event, organizer_actor: organizer, join_options: :restricted)
-
- organizer_participation =
- %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
-
- {:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor)
-
- join_data =
- File.read!("test/fixtures/mobilizon-leave-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", participant_url)
- |> Map.put("object", event_url)
-
- assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data)
-
- assert activity.data["object"] == event_url
- assert activity.data["actor"] == participant_url
-
- # The only participant left is the organizer
- assert event.id
- |> Events.list_participants_for_event()
- |> Map.get(:elements)
- |> Enum.map(& &1.id) ==
- [organizer_participation.id]
- end
-
- test "it refuses Leave activities when actor is the only organizer" do
- %Actor{url: organizer_url} = organizer = insert(:actor)
-
- %Event{url: event_url} =
- event = insert(:event, organizer_actor: organizer, join_options: :restricted)
-
- %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
-
- join_data =
- File.read!("test/fixtures/mobilizon-leave-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", organizer_url)
- |> Map.put("object", event_url)
-
- assert :error = Transmogrifier.handle_incoming(join_data)
- end
- end
-
- describe "handle Invite activities on group" do
- test "it accepts Invite activities" do
- %Actor{url: group_url, id: group_id} = group = insert(:group)
- %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor)
-
- %Member{} =
- _group_admin_member =
- insert(:member, parent: group, actor: group_admin, role: :administrator)
-
- %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor)
-
- invite_data =
- File.read!("test/fixtures/mobilizon-invite-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", group_admin_url)
- |> Map.put("object", group_url)
- |> Map.put("target", invitee_url)
-
- assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data)
- assert %Member{} = member = Actors.get_member_by_url(invite_data["id"])
- assert member.actor.id == invitee_id
- assert member.parent.id == group_id
- assert member.role == :invited
- assert member.invited_by_id == group_admin_id
- end
-
- test "it refuses Invite activities for " do
- %Actor{url: group_url, id: group_id} = group = insert(:group)
- %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor)
-
- %Member{} =
- _group_admin_member =
- insert(:member, parent: group, actor: group_admin, role: :administrator)
-
- %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor)
-
- invite_data =
- File.read!("test/fixtures/mobilizon-invite-activity.json")
- |> Jason.decode!()
- |> Map.put("actor", group_admin_url)
- |> Map.put("object", group_url)
- |> Map.put("target", invitee_url)
-
- assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data)
- assert %Member{} = member = Actors.get_member_by_url(invite_data["id"])
- assert member.actor.id == invitee_id
- assert member.parent.id == group_id
- assert member.role == :invited
- assert member.invited_by_id == group_admin_id
- end
- end
-
describe "prepare outgoing" do
test "it turns mentions into tags" do
actor = insert(:actor)
@@ -1501,7 +1058,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
assert Enum.member?(object["tag"], expected_mention)
end
- test "it adds the json-ld context and the conversation property" do
+ test "it adds the json-ld context and the discussion property" do
actor = insert(:actor)
{:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "hey"})
@@ -1558,24 +1115,40 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
describe "actor origin check" do
test "it rejects objects with a bogus origin" do
- use_cassette "activity_pub/object_bogus_origin" do
- {:error, _} = ActivityPub.fetch_object_from_url("https://info.pleroma.site/activity.json")
- end
+ data =
+ File.read!("test/fixtures/https__info.pleroma.site_activity.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://info.pleroma.site/activity.json"}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ {:error, _} = ActivityPub.fetch_object_from_url("https://info.pleroma.site/activity.json")
end
test "it rejects activities which reference objects with bogus origins" do
- use_cassette "activity_pub/activity_object_bogus" do
- data = %{
- "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => "https://framapiaf.org/users/admin/activities/1234",
- "actor" => "https://framapiaf.org/users/admin",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "object" => "https://info.pleroma.site/activity.json",
- "type" => "Announce"
- }
+ data =
+ File.read!("test/fixtures/https__info.pleroma.site_activity.json")
+ |> Jason.decode!()
- :error = Transmogrifier.handle_incoming(data)
- end
+ Mock
+ |> expect(:call, fn
+ %{method: :get, url: "https://info.pleroma.site/activity.json"}, _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => "https://framapiaf.org/users/admin/activities/1234",
+ "actor" => "https://framapiaf.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "object" => "https://info.pleroma.site/activity.json",
+ "type" => "Announce"
+ }
+
+ :error = Transmogrifier.handle_incoming(data)
end
end
end
diff --git a/test/federation/activity_pub/utils_test.exs b/test/federation/activity_pub/utils_test.exs
index 5c471f3e..35c81f07 100644
--- a/test/federation/activity_pub/utils_test.exs
+++ b/test/federation/activity_pub/utils_test.exs
@@ -10,15 +10,11 @@ defmodule Mobilizon.Federation.ActivityPub.UtilsTest do
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes
- setup_all do
- HTTPoison.start()
- end
-
describe "make" do
test "comment data from struct" do
comment = insert(:comment)
tag = insert(:tag, title: "MyTag")
- reply = insert(:comment, in_reply_to_comment: comment, tags: [tag])
+ reply = insert(:comment, in_reply_to_comment: comment, tags: [tag], attributed_to: nil)
assert %{
"type" => "Note",
@@ -42,8 +38,8 @@ defmodule Mobilizon.Federation.ActivityPub.UtilsTest do
end
test "comment data from map" do
- comment = insert(:comment)
- reply = insert(:comment, in_reply_to_comment: comment)
+ comment = insert(:comment, attributed_to: nil)
+ reply = insert(:comment, in_reply_to_comment: comment, attributed_to: nil)
to = ["https://www.w3.org/ns/activitystreams#Public"]
comment_data = Converter.Comment.model_to_as(reply)
assert comment_data["type"] == "Note"
diff --git a/test/fixtures/geospatial/addok/geocode.json b/test/fixtures/geospatial/addok/geocode.json
new file mode 100644
index 00000000..34cb8ab3
--- /dev/null
+++ b/test/fixtures/geospatial/addok/geocode.json
@@ -0,0 +1,31 @@
+{
+ "type": "FeatureCollection",
+ "version": "draft",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": { "type": "Point", "coordinates": [4.842569, 45.751718] },
+ "properties": {
+ "label": "10 Rue Jangot 69007 Lyon",
+ "score": 0.9999999999926557,
+ "housenumber": "10",
+ "id": "69387_3650_00010",
+ "type": "housenumber",
+ "x": 843232.29,
+ "y": 6518573.31,
+ "importance": 0.5454797306366062,
+ "name": "10 Rue Jangot",
+ "postcode": "69007",
+ "citycode": "69387",
+ "city": "Lyon",
+ "district": "Lyon 7e Arrondissement",
+ "context": "69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes",
+ "street": "Rue Jangot",
+ "distance": 0
+ }
+ }
+ ],
+ "attribution": "BAN",
+ "licence": "ETALAB-2.0",
+ "limit": 1
+}
diff --git a/test/fixtures/geospatial/addok/search.json b/test/fixtures/geospatial/addok/search.json
new file mode 100644
index 00000000..70cb287d
--- /dev/null
+++ b/test/fixtures/geospatial/addok/search.json
@@ -0,0 +1,49 @@
+{
+ "type": "FeatureCollection",
+ "version": "draft",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": { "type": "Point", "coordinates": [4.842569, 45.751718] },
+ "properties": {
+ "label": "10 Rue Jangot 69007 Lyon",
+ "score": 0.8677708846033279,
+ "housenumber": "10",
+ "id": "69387_3650_00010",
+ "type": "housenumber",
+ "x": 843232.29,
+ "y": 6518573.31,
+ "importance": 0.5454797306366062,
+ "name": "10 Rue Jangot",
+ "postcode": "69007",
+ "citycode": "69387",
+ "city": "Lyon",
+ "district": "Lyon 7e Arrondissement",
+ "context": "69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes",
+ "street": "Rue Jangot"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": { "type": "Point", "coordinates": [2.440319, 50.371266] },
+ "properties": {
+ "label": "Rue Jangon 62127 Bailleul-aux-Cornailles",
+ "score": 0.5269641371131077,
+ "id": "62070_0100",
+ "type": "street",
+ "x": 660129.18,
+ "y": 7030540.46,
+ "importance": 0.25814396978264664,
+ "name": "Rue Jangon",
+ "postcode": "62127",
+ "citycode": "62070",
+ "city": "Bailleul-aux-Cornailles",
+ "context": "62, Pas-de-Calais, Hauts-de-France"
+ }
+ }
+ ],
+ "attribution": "BAN",
+ "licence": "ETALAB-2.0",
+ "query": "10 Rue Jangot",
+ "limit": 5
+}
diff --git a/test/fixtures/geospatial/google_maps/api_key_invalid.json b/test/fixtures/geospatial/google_maps/api_key_invalid.json
new file mode 100644
index 00000000..6c0ff588
--- /dev/null
+++ b/test/fixtures/geospatial/google_maps/api_key_invalid.json
@@ -0,0 +1,5 @@
+{
+ "error_message": "The provided API key is invalid.",
+ "results": [],
+ "status": "REQUEST_DENIED"
+}
diff --git a/test/fixtures/geospatial/google_maps/geocode.json b/test/fixtures/geospatial/google_maps/geocode.json
new file mode 100644
index 00000000..232d1885
--- /dev/null
+++ b/test/fixtures/geospatial/google_maps/geocode.json
@@ -0,0 +1,109 @@
+{
+ "plus_code": {
+ "compound_code": "QR2V+M2 Lyon, France",
+ "global_code": "8FQ6QR2V+M2"
+ },
+ "results": [
+ {
+ "address_components": [
+ {
+ "long_name": "10bis",
+ "short_name": "10bis",
+ "types": ["street_number"]
+ },
+ {
+ "long_name": "Rue Jangot",
+ "short_name": "Rue Jangot",
+ "types": ["route"]
+ },
+ {
+ "long_name": "Lyon",
+ "short_name": "Lyon",
+ "types": ["locality", "political"]
+ },
+ {
+ "long_name": "Rhône",
+ "short_name": "Rhône",
+ "types": ["administrative_area_level_2", "political"]
+ },
+ {
+ "long_name": "Auvergne-Rhône-Alpes",
+ "short_name": "Auvergne-Rhône-Alpes",
+ "types": ["administrative_area_level_1", "political"]
+ },
+ {
+ "long_name": "France",
+ "short_name": "FR",
+ "types": ["country", "political"]
+ },
+ {
+ "long_name": "69007",
+ "short_name": "69007",
+ "types": ["postal_code"]
+ }
+ ],
+ "formatted_address": "10bis Rue Jangot, 69007 Lyon, France",
+ "geometry": {
+ "location": { "lat": 45.751725, "lng": 4.8424966 },
+ "location_type": "ROOFTOP",
+ "viewport": {
+ "northeast": { "lat": 45.7530739802915, "lng": 4.843845580291503 },
+ "southwest": { "lat": 45.7503760197085, "lng": 4.841147619708499 }
+ }
+ },
+ "place_id": "ChIJrW0QikTq9EcR96jk2OnO75w",
+ "plus_code": {
+ "compound_code": "QR2R+MX Lyon, France",
+ "global_code": "8FQ6QR2R+MX"
+ },
+ "types": ["street_address"]
+ },
+ {
+ "address_components": [
+ { "long_name": "9", "short_name": "9", "types": ["street_number"] },
+ {
+ "long_name": "Rue Jangot",
+ "short_name": "Rue Jangot",
+ "types": ["route"]
+ },
+ {
+ "long_name": "Lyon",
+ "short_name": "Lyon",
+ "types": ["locality", "political"]
+ },
+ {
+ "long_name": "Rhône",
+ "short_name": "Rhône",
+ "types": ["administrative_area_level_2", "political"]
+ },
+ {
+ "long_name": "Auvergne-Rhône-Alpes",
+ "short_name": "Auvergne-Rhône-Alpes",
+ "types": ["administrative_area_level_1", "political"]
+ },
+ {
+ "long_name": "France",
+ "short_name": "FR",
+ "types": ["country", "political"]
+ },
+ {
+ "long_name": "69007",
+ "short_name": "69007",
+ "types": ["postal_code"]
+ }
+ ],
+ "formatted_address": "9 Rue Jangot, 69007 Lyon, France",
+ "geometry": {
+ "location": { "lat": 45.7518165, "lng": 4.8427168 },
+ "location_type": "RANGE_INTERPOLATED",
+ "viewport": {
+ "northeast": { "lat": 45.7531654802915, "lng": 4.844065780291502 },
+ "southwest": { "lat": 45.7504675197085, "lng": 4.841367819708497 }
+ }
+ },
+ "place_id": "EiA5IFJ1ZSBKYW5nb3QsIDY5MDA3IEx5b24sIEZyYW5jZSIaEhgKFAoSCR8N2ItE6vRHEW9tyPnhQsUIEAk",
+ "types": ["street_address"]
+ }
+ ],
+ "status": "OK"
+}
diff --git a/test/fixtures/geospatial/google_maps/geocode_2.json b/test/fixtures/geospatial/google_maps/geocode_2.json
new file mode 100644
index 00000000..9ab5debf
--- /dev/null
+++ b/test/fixtures/geospatial/google_maps/geocode_2.json
@@ -0,0 +1,62 @@
+{
+ "html_attributions": [],
+ "result": {
+ "address_components": [
+ {
+ "long_name": "10bis",
+ "short_name": "10bis",
+ "types": ["street_number"]
+ },
+ {
+ "long_name": "Rue Jangot",
+ "short_name": "Rue Jangot",
+ "types": ["route"]
+ },
+ {
+ "long_name": "Lyon",
+ "short_name": "Lyon",
+ "types": ["locality", "political"]
+ },
+ {
+ "long_name": "Rhône",
+ "short_name": "Rhône",
+ "types": ["administrative_area_level_2", "political"]
+ },
+ {
+ "long_name": "Auvergne-Rhône-Alpes",
+ "short_name": "Auvergne-Rhône-Alpes",
+ "types": ["administrative_area_level_1", "political"]
+ },
+ {
+ "long_name": "France",
+ "short_name": "FR",
+ "types": ["country", "political"]
+ },
+ { "long_name": "69007", "short_name": "69007", "types": ["postal_code"] }
+ ],
+ "adr_address": "\u003cspan class=\"street-address\"\u003e10bis Rue Jangot\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e69007\u003c/span\u003e \u003cspan class=\"locality\"\u003eLyon\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eFrance\u003c/span\u003e",
+ "formatted_address": "10bis Rue Jangot, 69007 Lyon, France",
+ "geometry": {
+ "location": { "lat": 45.751725, "lng": 4.8424966 },
+ "viewport": {
+ "northeast": { "lat": 45.7531097802915, "lng": 4.843951380291502 },
+ "southwest": { "lat": 45.7504118197085, "lng": 4.841253419708497 }
+ }
+ },
+ "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png",
+ "id": "4a3482a7a74c6203048adf713b736186c4ace7cd",
+ "name": "10bis Rue Jangot",
+ "place_id": "ChIJrW0QikTq9EcR96jk2OnO75w",
+ "plus_code": {
+ "compound_code": "QR2R+MX Lyon, France",
+ "global_code": "8FQ6QR2R+MX"
+ },
+ "reference": "ChIJrW0QikTq9EcR96jk2OnO75w",
+ "scope": "GOOGLE",
+ "types": ["street_address"],
+ "url": "https://maps.google.com/?q=10bis+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106dad:0x9cefcee9d8e4a8f7",
+ "utc_offset": 120,
+ "vicinity": "Lyon"
+ },
+ "status": "OK"
+}
diff --git a/test/fixtures/geospatial/google_maps/search.json b/test/fixtures/geospatial/google_maps/search.json
new file mode 100644
index 00000000..8d6c2f52
--- /dev/null
+++ b/test/fixtures/geospatial/google_maps/search.json
@@ -0,0 +1,55 @@
+{
+ "results": [
+ {
+ "address_components": [
+ { "long_name": "10", "short_name": "10", "types": ["street_number"] },
+ {
+ "long_name": "Rue Jangot",
+ "short_name": "Rue Jangot",
+ "types": ["route"]
+ },
+ {
+ "long_name": "Lyon",
+ "short_name": "Lyon",
+ "types": ["locality", "political"]
+ },
+ {
+ "long_name": "Rhône",
+ "short_name": "Rhône",
+ "types": ["administrative_area_level_2", "political"]
+ },
+ {
+ "long_name": "Auvergne-Rhône-Alpes",
+ "short_name": "Auvergne-Rhône-Alpes",
+ "types": ["administrative_area_level_1", "political"]
+ },
+ {
+ "long_name": "France",
+ "short_name": "FR",
+ "types": ["country", "political"]
+ },
+ {
+ "long_name": "69007",
+ "short_name": "69007",
+ "types": ["postal_code"]
+ }
+ ],
+ "formatted_address": "10 Rue Jangot, 69007 Lyon, France",
+ "geometry": {
+ "location": { "lat": 45.75164940000001, "lng": 4.8424032 },
+ "location_type": "ROOFTOP",
+ "viewport": {
+ "northeast": { "lat": 45.75299838029151, "lng": 4.843752180291502 },
+ "southwest": { "lat": 45.75030041970851, "lng": 4.841054219708497 }
+ }
+ },
+ "place_id": "ChIJtW0QikTq9EcRLI4Vy6bRx0U",
+ "plus_code": {
+ "compound_code": "QR2R+MX Lyon, France",
+ "global_code": "8FQ6QR2R+MX"
+ },
+ "types": ["street_address"]
+ }
+ ],
+ "status": "OK"
+}
diff --git a/test/fixtures/geospatial/google_maps/search_2.json b/test/fixtures/geospatial/google_maps/search_2.json
new file mode 100644
index 00000000..93856701
--- /dev/null
+++ b/test/fixtures/geospatial/google_maps/search_2.json
@@ -0,0 +1,58 @@
+{
+ "html_attributions": [],
+ "result": {
+ "address_components": [
+ { "long_name": "10", "short_name": "10", "types": ["street_number"] },
+ {
+ "long_name": "Rue Jangot",
+ "short_name": "Rue Jangot",
+ "types": ["route"]
+ },
+ {
+ "long_name": "Lyon",
+ "short_name": "Lyon",
+ "types": ["locality", "political"]
+ },
+ {
+ "long_name": "Rhône",
+ "short_name": "Rhône",
+ "types": ["administrative_area_level_2", "political"]
+ },
+ {
+ "long_name": "Auvergne-Rhône-Alpes",
+ "short_name": "Auvergne-Rhône-Alpes",
+ "types": ["administrative_area_level_1", "political"]
+ },
+ {
+ "long_name": "France",
+ "short_name": "FR",
+ "types": ["country", "political"]
+ },
+ { "long_name": "69007", "short_name": "69007", "types": ["postal_code"] }
+ ],
+ "adr_address": "\u003cspan class=\"street-address\"\u003e10 Rue Jangot\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e69007\u003c/span\u003e \u003cspan class=\"locality\"\u003eLyon\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eFrance\u003c/span\u003e",
+ "formatted_address": "10 Rue Jangot, 69007 Lyon, France",
+ "geometry": {
+ "location": { "lat": 45.75164940000001, "lng": 4.842403200000001 },
+ "viewport": {
+ "northeast": { "lat": 45.7530412802915, "lng": 4.843668630291503 },
+ "southwest": { "lat": 45.75034331970851, "lng": 4.840970669708498 }
+ }
+ },
+ "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png",
+ "id": "61b9418d092d2ed05ddd65a55dddefda5b9628cc",
+ "name": "10 Rue Jangot",
+ "place_id": "ChIJtW0QikTq9EcRLI4Vy6bRx0U",
+ "plus_code": {
+ "compound_code": "QR2R+MX Lyon, France",
+ "global_code": "8FQ6QR2R+MX"
+ },
+ "reference": "ChIJtW0QikTq9EcRLI4Vy6bRx0U",
+ "scope": "GOOGLE",
+ "types": ["street_address"],
+ "url": "https://maps.google.com/?q=10+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106db5:0x45c7d1a6cb158e2c",
+ "utc_offset": 120,
+ "vicinity": "Lyon"
+ },
+ "status": "OK"
+}
diff --git a/test/fixtures/geospatial/map_quest/geocode.json b/test/fixtures/geospatial/map_quest/geocode.json
new file mode 100644
index 00000000..822ccc93
--- /dev/null
+++ b/test/fixtures/geospatial/map_quest/geocode.json
@@ -0,0 +1,43 @@
+{
+ "info": {
+ "statuscode": 0,
+ "copyright": {
+ "text": "\\u00A9 2019 MapQuest, Inc.",
+ "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+ "imageAltText": "\\u00A9 2019 MapQuest, Inc."
+ },
+ "messages": []
+ },
+ "options": { "maxResults": 1, "thumbMaps": true, "ignoreLatLngInput": false },
+ "results": [
+ {
+ "providedLocation": { "latLng": { "lat": 45.751718, "lng": 4.842569 } },
+ "locations": [
+ {
+ "street": "10 Rue Jangot",
+ "adminArea6": "",
+ "adminArea6Type": "Neighborhood",
+ "adminArea5": "Lyon",
+ "adminArea5Type": "City",
+ "adminArea4": "",
+ "adminArea4Type": "County",
+ "adminArea3": "Auvergne-Rhône-Alpes",
+ "adminArea3Type": "State",
+ "adminArea1": "FR",
+ "adminArea1Type": "Country",
+ "postalCode": "69007",
+ "geocodeQualityCode": "P1AAA",
+ "geocodeQuality": "POINT",
+ "dragPoint": false,
+ "sideOfStreet": "N",
+ "linkId": "0",
+ "unknownInput": "",
+ "type": "s",
+ "latLng": { "lat": 45.751714, "lng": 4.842566 },
+ "displayLatLng": { "lat": 45.751714, "lng": 4.842566 },
+ "mapUrl": "http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433"
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/geospatial/map_quest/search.json b/test/fixtures/geospatial/map_quest/search.json
new file mode 100644
index 00000000..d58195ee
--- /dev/null
+++ b/test/fixtures/geospatial/map_quest/search.json
@@ -0,0 +1,47 @@
+{
+ "info": {
+ "statuscode": 0,
+ "copyright": {
+ "text": "\\u00A9 2019 MapQuest, Inc.",
+ "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+ "imageAltText": "\\u00A9 2019 MapQuest, Inc."
+ },
+ "messages": []
+ },
+ "options": {
+ "maxResults": 10,
+ "thumbMaps": true,
+ "ignoreLatLngInput": false
+ },
+ "results": [
+ {
+ "providedLocation": { "location": "10 rue Jangot" },
+ "locations": [
+ {
+ "street": "10 Rue Jangot",
+ "adminArea6": "7e",
+ "adminArea6Type": "Neighborhood",
+ "adminArea5": "Lyon",
+ "adminArea5Type": "City",
+ "adminArea4": "Lyon",
+ "adminArea4Type": "County",
+ "adminArea3": "Auvergne-Rhône-Alpes",
+ "adminArea3Type": "State",
+ "adminArea1": "FR",
+ "adminArea1Type": "Country",
+ "postalCode": "69007",
+ "geocodeQualityCode": "P1AXX",
+ "geocodeQuality": "POINT",
+ "dragPoint": false,
+ "sideOfStreet": "N",
+ "linkId": "0",
+ "unknownInput": "",
+ "type": "s",
+ "latLng": { "lat": 45.751714, "lng": 4.842566 },
+ "displayLatLng": { "lat": 45.751714, "lng": 4.842566 },
+ "mapUrl": "http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752"
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/geospatial/nominatim/geocode.json b/test/fixtures/geospatial/nominatim/geocode.json
new file mode 100644
index 00000000..ed712f68
--- /dev/null
+++ b/test/fixtures/geospatial/nominatim/geocode.json
@@ -0,0 +1,43 @@
+{
+ "type": "FeatureCollection",
+ "geocoding": {
+ "version": "0.1.0",
+ "attribution": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
+ "licence": "ODbL",
+ "query": "45.751718,4.842569"
+ },
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {
+ "geocoding": {
+ "place_id": 41453794,
+ "osm_type": "node",
+ "osm_id": 3078260611,
+ "type": "house",
+ "accuracy": 0,
+ "label": "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France",
+ "name": null,
+ "housenumber": "10",
+ "street": "Rue Jangot",
+ "postcode": "69007",
+ "city": "Lyon",
+ "county": "Lyon",
+ "state": "Auvergne-Rhône-Alpes",
+ "country": "France",
+ "admin": {
+ "level2": "France",
+ "level3": "Metropolitan France",
+ "level4": "Auvergne-Rhône-Alpes",
+ "level5": "Departemental constituency of Rhône",
+ "level6": "Métropole de Lyon",
+ "level7": "Lyon",
+ "level8": "Lyon",
+ "level9": "Lyon 7e Arrondissement"
+ }
+ }
+ },
+ "geometry": { "type": "Point", "coordinates": [4.8425657, 45.7517141] }
+ }
+ ]
+}
diff --git a/test/fixtures/geospatial/nominatim/search.json b/test/fixtures/geospatial/nominatim/search.json
new file mode 100644
index 00000000..e03324fa
--- /dev/null
+++ b/test/fixtures/geospatial/nominatim/search.json
@@ -0,0 +1,42 @@
+{
+ "type": "FeatureCollection",
+ "geocoding": {
+ "version": "0.1.0",
+ "attribution": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
+ "licence": "ODbL",
+ "query": "10 rue Jangot"
+ },
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {
+ "geocoding": {
+ "place_id": 41453794,
+ "osm_type": "node",
+ "osm_id": 3078260611,
+ "type": "house",
+ "label": "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France",
+ "name": null,
+ "housenumber": "10",
+ "street": "Rue Jangot",
+ "postcode": "69007",
+ "city": "Lyon",
+ "county": "Lyon",
+ "state": "Auvergne-Rhône-Alpes",
+ "country": "France",
+ "admin": {
+ "level2": "France",
+ "level3": "Metropolitan France",
+ "level4": "Auvergne-Rhône-Alpes",
+ "level5": "Departemental constituency of Rhône",
+ "level6": "Métropole de Lyon",
+ "level7": "Lyon",
+ "level8": "Lyon",
+ "level9": "Lyon 7e Arrondissement"
+ }
+ }
+ },
+ "geometry": { "type": "Point", "coordinates": [4.8425657, 45.7517141] }
+ }
+ ]
+}
diff --git a/test/fixtures/geospatial/photon/search.json b/test/fixtures/geospatial/photon/search.json
new file mode 100644
index 00000000..7a775eaa
--- /dev/null
+++ b/test/fixtures/geospatial/photon/search.json
@@ -0,0 +1,37 @@
+{
+ "features": [
+ {
+ "geometry": { "coordinates": [4.8425657, 45.7517141], "type": "Point" },
+ "type": "Feature",
+ "properties": {
+ "osm_id": 3078260611,
+ "osm_type": "N",
+ "country": "France",
+ "osm_key": "place",
+ "housenumber": "10",
+ "city": "Lyon",
+ "street": "Rue Jangot",
+ "osm_value": "house",
+ "postcode": "69007",
+ "state": "Auvergne-Rhône-Alpes"
+ }
+ },
+ {
+ "geometry": { "coordinates": [4.8424254, 45.7517056], "type": "Point" },
+ "type": "Feature",
+ "properties": {
+ "osm_id": 3078260612,
+ "osm_type": "N",
+ "country": "France",
+ "osm_key": "place",
+ "housenumber": "10bis",
+ "city": "Lyon",
+ "street": "Rue Jangot",
+ "osm_value": "house",
+ "postcode": "69007",
+ "state": "Auvergne-Rhône-Alpes"
+ }
+ }
+ ],
+ "type": "FeatureCollection"
+}
diff --git a/test/fixtures/https__info.pleroma.site_activity.json b/test/fixtures/https__info.pleroma.site_activity.json
new file mode 100644
index 00000000..ae18eb7b
--- /dev/null
+++ b/test/fixtures/https__info.pleroma.site_activity.json
@@ -0,0 +1,12 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "http://mastodon.example.org/users/admin",
+ "attachment": [],
+ "attributedTo": "http://mastodon.example.org/users/admin",
+ "content": "
this post was not actually written by Haelwenn
",
+ "id": "https://info.pleroma.site/activity.json",
+ "published": "2018-09-01T22:15:00Z",
+ "tag": [],
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "type": "Note"
+}
diff --git a/test/fixtures/mastodon-actor.json b/test/fixtures/mastodon-actor.json
new file mode 100644
index 00000000..70d41e92
--- /dev/null
+++ b/test/fixtures/mastodon-actor.json
@@ -0,0 +1,91 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "toot": "http://joinmastodon.org/ns#",
+ "featured": {
+ "@id": "toot:featured",
+ "@type": "@id"
+ },
+ "alsoKnownAs": {
+ "@id": "as:alsoKnownAs",
+ "@type": "@id"
+ },
+ "movedTo": {
+ "@id": "as:movedTo",
+ "@type": "@id"
+ },
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "IdentityProof": "toot:IdentityProof",
+ "discoverable": "toot:discoverable",
+ "Device": "toot:Device",
+ "Ed25519Signature": "toot:Ed25519Signature",
+ "Ed25519Key": "toot:Ed25519Key",
+ "Curve25519Key": "toot:Curve25519Key",
+ "EncryptedMessage": "toot:EncryptedMessage",
+ "publicKeyBase64": "toot:publicKeyBase64",
+ "deviceId": "toot:deviceId",
+ "claim": {
+ "@type": "@id",
+ "@id": "toot:claim"
+ },
+ "fingerprintKey": {
+ "@type": "@id",
+ "@id": "toot:fingerprintKey"
+ },
+ "identityKey": {
+ "@type": "@id",
+ "@id": "toot:identityKey"
+ },
+ "devices": {
+ "@type": "@id",
+ "@id": "toot:devices"
+ },
+ "messageFranking": "toot:messageFranking",
+ "messageType": "toot:messageType",
+ "cipherText": "toot:cipherText",
+ "focalPoint": {
+ "@container": "@list",
+ "@id": "toot:focalPoint"
+ }
+ }
+ ],
+ "id": "https://framapiaf.org/users/peertube",
+ "type": "Person",
+ "following": "https://framapiaf.org/users/peertube/following",
+ "followers": "https://framapiaf.org/users/peertube/followers",
+ "inbox": "https://framapiaf.org/users/peertube/inbox",
+ "outbox": "https://framapiaf.org/users/peertube/outbox",
+ "featured": "https://framapiaf.org/users/peertube/collections/featured",
+ "preferredUsername": "peertube",
+ "name": "PeerTube",
+ "summary": "
PeerTube Software Official Account - No support here // Compte officiel du logiciel PeerTube (animé par Framasoft). Nous ne faisons pas de support depuis ce compte. Merci de contacter l'administrateur⋅ice de l'instance concernée ou de vous rendre sur https://framacolibri.org/c/peertube
",
+ "url": "https://framapiaf.org/@peertube",
+ "manuallyApprovesFollowers": false,
+ "discoverable": false,
+ "devices": "https://framapiaf.org/users/peertube/collections/devices",
+ "publicKey": {
+ "id": "https://framapiaf.org/users/peertube#main-key",
+ "owner": "https://framapiaf.org/users/peertube",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn1y53r+ymOmDoP8iYxa1\nb1VvXkldZVpxJg1ZVeq4SijVS3oNurrQQhpTwTmCCAue2m+UvG4eEEYAYSfb5+C3\nbqH3kLlQrptkp8y/qz3d4tk/b8RConAaws7/SwksDC5rs+cYLnnXgD7rAaT1uH/B\nVTzG79YLgnasK6IxpnBth6Vru+9g2U8PzIUOfuwPV3aZeu9q2xEdC5/GnnjsfKZv\nWEzpG3HkRAlaTRDYadl9dWOPlfhy/LMkknAP02j+Qt/s7y83YqsrUyvQcfTSy3Zf\nLNNFrpU4u1ACyZXzvaoDXQH8HetKSA06xqa4pJO4xmM2PWMoBq1KX3Us4sP291w4\nEQIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "tag": [],
+ "attachment": [],
+ "endpoints": {
+ "sharedInbox": "https://framapiaf.org/inbox"
+ },
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/223/824/original/03ed95406a9a3cd0.png"
+ },
+ "image": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/223/824/original/2fbb4d6268c2fb20.png"
+ }
+}
diff --git a/test/fixtures/mastodon-announce.json b/test/fixtures/mastodon-announce.json
index 83da31bb..16c2bb17 100644
--- a/test/fixtures/mastodon-announce.json
+++ b/test/fixtures/mastodon-announce.json
@@ -1,8 +1,6 @@
{
"type": "Announce",
- "to": [
- "https://www.w3.org/ns/activitystreams#Public"
- ],
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
"signature": {
"type": "RsaSignature2017",
"signatureValue": "T95DRE0eAligvMuRMkQA01lsoz2PKi4XXF+cyZ0BqbrO12p751TEWTyyRn5a+HH0e4kc77EUhQVXwMq80WAYDzHKVUTf2XBJPBa68vl0j6RXw3+HK4ef5hR4KWFNBU34yePS7S1fEmc1mTG4Yx926wtmZwDpEMTp1CXOeVEjCYzmdyHpepPPH2ZZettiacmPRSqBLPGWZoot7kH/SioIdnrMGY0I7b+rqkIdnnEcdhu9N1BKPEO9Sr+KmxgAUiidmNZlbBXX6gCxp8BiIdH4ABsIcwoDcGNkM5EmWunGW31LVjsEQXhH5c1Wly0ugYYPCg/0eHLNBOhKkY/teSM8Lg==",
@@ -10,14 +8,14 @@
"created": "2018-02-17T19:39:15Z"
},
"published": "2018-02-17T19:39:15Z",
- "object": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400",
- "id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
+ "object": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity",
"cc": [
- "https://framapiaf.org/users/Framasoft",
- "https://framapiaf.org/users/Framasoft/followers"
+ "https://framapiaf.org/users/peertube",
+ "https://framapiaf.org/users/peertube/followers"
],
- "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
- "actor": "https://framapiaf.org/users/Framasoft",
+ "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity",
+ "actor": "https://framapiaf.org/users/peertube",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
diff --git a/test/fixtures/mastodon-status-2.json b/test/fixtures/mastodon-status-2.json
new file mode 100644
index 00000000..fe58c7fa
--- /dev/null
+++ b/test/fixtures/mastodon-status-2.json
@@ -0,0 +1,68 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "blurhash": "toot:blurhash",
+ "focalPoint": {
+ "@container": "@list",
+ "@id": "toot:focalPoint"
+ },
+ "Hashtag": "as:Hashtag"
+ }
+ ],
+ "id": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2019-05-14T09:13:13Z",
+ "url": "https://framapiaf.org/@Framasoft/102093631881522097",
+ "attributedTo": "https://framapiaf.org/users/Framasoft",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": ["https://framapiaf.org/users/Framasoft/followers"],
+ "sensitive": false,
+ "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:framapiaf.org,2019-05-14:objectId=16200311:objectType=Conversation",
+ "content": "
Mobilizon : Finançons un outil pour sortir nos événements de Facebook !
https://framablog.org/2019/05/14/mobilizon-financons-un-outil-pour-sortir-nos-evenements-de-facebook/ #Framablog #TootOuRien
Nous avons moins de 60 jours pour financer Mobilizon. Moins de 60 jours pour faire connaître notre projet d'alternative libre et fédérée aux événements Facebook ; et pour savoir à quel point nous devons nous y investir.
Changer le logiciel de celles et ceux qui changent le …
",
+ "contentMap": {
+ "fr": "
Mobilizon : Finançons un outil pour sortir nos événements de Facebook !
https://framablog.org/2019/05/14/mobilizon-financons-un-outil-pour-sortir-nos-evenements-de-facebook/ #Framablog #TootOuRien
Nous avons moins de 60 jours pour financer Mobilizon. Moins de 60 jours pour faire connaître notre projet d'alternative libre et fédérée aux événements Facebook ; et pour savoir à quel point nous devons nous y investir.
Changer le logiciel de celles et ceux qui changent le …
"
+ },
+ "attachment": [
+ {
+ "type": "Document",
+ "mediaType": "image/jpeg",
+ "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/003/337/144/original/39d457fd9e0f0171.jpg",
+ "name": null,
+ "blurhash": "UIF=jrpIM|~q~VT0%2t6Ne9a?G-;9ZRP%2Rk"
+ }
+ ],
+ "tag": [
+ {
+ "type": "Hashtag",
+ "href": "https://framapiaf.org/tags/framablog",
+ "name": "#framablog"
+ },
+ {
+ "type": "Hashtag",
+ "href": "https://framapiaf.org/tags/tootourien",
+ "name": "#tootourien"
+ }
+ ],
+ "replies": {
+ "id": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies?only_other_accounts=true&page=true",
+ "partOf": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/mastodon-status-3.json b/test/fixtures/mastodon-status-3.json
new file mode 100644
index 00000000..a650d063
--- /dev/null
+++ b/test/fixtures/mastodon-status-3.json
@@ -0,0 +1,34 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://zoltasila.pl/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "actor": "https://zoltasila.pl/users/mkljczk",
+ "attachment": [],
+ "attributedTo": "https://zoltasila.pl/users/mkljczk",
+ "cc": ["https://zoltasila.pl/users/mkljczk/followers"],
+ "content": "
@peertube guess you wanted to put the en_US lang link
",
+ "context": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation",
+ "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation",
+ "id": "https://zoltasila.pl/objects/1c295713-8e3c-411e-9e62-57a7b9c9e514",
+ "inReplyTo": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "published": "2020-07-27T09:37:57.202806Z",
+ "sensitive": false,
+ "source": "@peertube@framapiaf.org guess you wanted to put the [en_US lang link](https://joinpeertube.org/en_US/news#release-2-3-0)",
+ "summary": "",
+ "tag": [
+ {
+ "href": "https://framapiaf.org/users/peertube",
+ "name": "@peertube@framapiaf.org",
+ "type": "Mention"
+ }
+ ],
+ "to": [
+ "https://framapiaf.org/users/peertube",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Note"
+}
diff --git a/test/fixtures/mastodon-status-4.json b/test/fixtures/mastodon-status-4.json
new file mode 100644
index 00000000..31e70078
--- /dev/null
+++ b/test/fixtures/mastodon-status-4.json
@@ -0,0 +1,56 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "blurhash": "toot:blurhash",
+ "focalPoint": {
+ "@container": "@list",
+ "@id": "toot:focalPoint"
+ }
+ }
+ ],
+ "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2020-07-27T07:19:11Z",
+ "url": "https://framapiaf.org/@peertube/104584600044284729",
+ "attributedTo": "https://framapiaf.org/users/peertube",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": ["https://framapiaf.org/users/peertube/followers"],
+ "sensitive": false,
+ "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation",
+ "content": "
PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!
Have you seen the broadcast message system ? 🤩
",
+ "contentMap": {
+ "en": "
PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!
Have you seen the broadcast message system ? 🤩
"
+ },
+ "attachment": [
+ {
+ "type": "Document",
+ "mediaType": "image/png",
+ "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/104/584/599/807/860/387/original/88c94143f78fdfa3.png",
+ "name": null,
+ "blurhash": "U5SY?Z00nOxu7ORP.8-pU^kVS#NGXyxbMxM{"
+ }
+ ],
+ "tag": [],
+ "replies": {
+ "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies?only_other_accounts=true&page=true",
+ "partOf": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/mastodon-status-5.json b/test/fixtures/mastodon-status-5.json
new file mode 100644
index 00000000..ff19c613
--- /dev/null
+++ b/test/fixtures/mastodon-status-5.json
@@ -0,0 +1,58 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount"
+ }
+ ],
+ "id": "https://diaspodon.fr/users/dada/statuses/100820008426311925",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d",
+ "published": "2018-10-01T10:54:01Z",
+ "url": "https://diaspodon.fr/@dada/100820008426311925",
+ "attributedTo": "https://diaspodon.fr/users/dada",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": [
+ "https://diaspodon.fr/users/dada/followers",
+ "https://framapiaf.org/users/Pouhiou",
+ "https://framatube.org/accounts/framasoft"
+ ],
+ "sensitive": false,
+ "atomUri": "https://diaspodon.fr/users/dada/statuses/100820008426311925",
+ "inReplyToAtomUri": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d",
+ "conversation": "tag:diaspodon.fr,2018-10-01:objectId=1187066:objectType=Conversation",
+ "content": "
@framasoft Ça ne serait pas la voix de @Pouhiou ? 🤔
",
+ "contentMap": {
+ "fr": "
@framasoft Ça ne serait pas la voix de @Pouhiou ? 🤔
"
+ },
+ "attachment": [],
+ "tag": [
+ {
+ "type": "Mention",
+ "href": "https://framatube.org/accounts/framasoft",
+ "name": "@framasoft@framatube.org"
+ },
+ {
+ "type": "Mention",
+ "href": "https://framapiaf.org/users/Pouhiou",
+ "name": "@Pouhiou@framapiaf.org"
+ }
+ ],
+ "replies": {
+ "id": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies?only_other_accounts=true&page=true",
+ "partOf": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/mastodon-status.json b/test/fixtures/mastodon-status.json
new file mode 100644
index 00000000..31e70078
--- /dev/null
+++ b/test/fixtures/mastodon-status.json
@@ -0,0 +1,56 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "blurhash": "toot:blurhash",
+ "focalPoint": {
+ "@container": "@list",
+ "@id": "toot:focalPoint"
+ }
+ }
+ ],
+ "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2020-07-27T07:19:11Z",
+ "url": "https://framapiaf.org/@peertube/104584600044284729",
+ "attributedTo": "https://framapiaf.org/users/peertube",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": ["https://framapiaf.org/users/peertube/followers"],
+ "sensitive": false,
+ "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation",
+ "content": "
PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!
Have you seen the broadcast message system ? 🤩
",
+ "contentMap": {
+ "en": "
PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!
Have you seen the broadcast message system ? 🤩
"
+ },
+ "attachment": [
+ {
+ "type": "Document",
+ "mediaType": "image/png",
+ "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/104/584/599/807/860/387/original/88c94143f78fdfa3.png",
+ "name": null,
+ "blurhash": "U5SY?Z00nOxu7ORP.8-pU^kVS#NGXyxbMxM{"
+ }
+ ],
+ "tag": [],
+ "replies": {
+ "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies?only_other_accounts=true&page=true",
+ "partOf": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/mobilizon-post-activity-group.json b/test/fixtures/mobilizon-post-activity-group.json
new file mode 100644
index 00000000..35444b40
--- /dev/null
+++ b/test/fixtures/mobilizon-post-activity-group.json
@@ -0,0 +1,52 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://litepub.social/litepub/context.jsonld",
+ {
+ "Hashtag": "as:Hashtag",
+ "category": "sc:category",
+ "ical": "http://www.w3.org/2002/12/cal/ical#",
+ "joinMode": {
+ "@id": "mz:joinMode",
+ "@type": "mz:joinModeType"
+ },
+ "joinModeType": {
+ "@id": "mz:joinModeType",
+ "@type": "rdfs:Class"
+ },
+ "maximumAttendeeCapacity": "sc:maximumAttendeeCapacity",
+ "mz": "https://joinmobilizon.org/ns#",
+ "repliesModerationOption": {
+ "@id": "mz:repliesModerationOption",
+ "@type": "mz:repliesModerationOptionType"
+ },
+ "repliesModerationOptionType": {
+ "@id": "mz:repliesModerationOptionType",
+ "@type": "rdfs:Class"
+ },
+ "sc": "http://schema.org#",
+ "uuid": "sc:identifier"
+ }
+ ],
+ "actor": "http://mobilizon1.com/@user",
+ "attributedTo": "http://mobilizon1.com/@group",
+ "cc": [],
+ "id": "http://mobilizon1.com/events/f270ae07-7991-453c-9bb7-3d2122ededae/activity",
+ "object": {
+ "actor": "http://mobilizon1.com/@user",
+ "attributedTo": "http://mobilizon1.com/@group",
+ "startTime": "2018-02-12T14:08:20Z",
+ "cc": [],
+ "content": "
@tcit
",
+ "id": "http://mobilizon1.com/events/f270ae07-7991-453c-9bb7-3d2122ededae",
+ "name": "My first event",
+ "published": "2018-02-12T14:08:20Z",
+ "tag": [],
+ "to": ["http://mobilizon1.com/@group"],
+ "type": "Event",
+ "uuid": "f270ae07-7991-453c-9bb7-3d2122ededae"
+ },
+ "published": "2018-02-12T14:08:20Z",
+ "to": ["http://mobilizon1.com/@group"],
+ "type": "Create"
+}
diff --git a/test/fixtures/peertube-video.json b/test/fixtures/peertube-video.json
new file mode 100644
index 00000000..fb36f2c5
--- /dev/null
+++ b/test/fixtures/peertube-video.json
@@ -0,0 +1,393 @@
+{
+ "type": "Video",
+ "id": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d",
+ "name": "What is PeerTube?",
+ "duration": "PT113S",
+ "uuid": "9c9de5e8-0a1e-484a-b099-e80766180a6d",
+ "tag": [
+ {
+ "type": "Hashtag",
+ "name": "framasoft"
+ },
+ {
+ "type": "Hashtag",
+ "name": "peertube"
+ }
+ ],
+ "category": {
+ "identifier": "15",
+ "name": "Science & Technology"
+ },
+ "licence": {
+ "identifier": "2",
+ "name": "Attribution - Share Alike"
+ },
+ "language": {
+ "identifier": "en",
+ "name": "English"
+ },
+ "views": 53137,
+ "sensitive": false,
+ "waitTranscoding": true,
+ "state": 1,
+ "commentsEnabled": true,
+ "downloadEnabled": true,
+ "published": "2018-10-01T10:52:46.396Z",
+ "originallyPublishedAt": null,
+ "updated": "2020-07-30T08:01:00.836Z",
+ "mediaType": "text/markdown",
+ "content": "**[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**\r\n*A decentralized video hosting network, based on free/libre software!*\r\n\r\n**Animation Produced by:** [LILA](https://libreart.info) - [ZeMarmot Team](https://film.zemarmot.net)\r\n*Directed by* Aryeom\r\n*Assistant* Jehan\r\n**Licence**: [CC-By-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\r\n\r\n**Sponsored by** [Framasoft](https://framasoft.org)\r\n\r\n**Music**: [Red Step Forward](http://play.dogmazic.net/song.php?song_id=52491) - CC-By Ken Bushima\r\n\r\n**Movie Clip**: [Caminades 3: Llamigos](http://www.caminandes.com/) CC-By Blender Institute\r\n\r\n**Video sources**: https://gitlab.gnome.org/Jehan/what-is-peertube/",
+ "support": null,
+ "subtitleLanguage": [
+ {
+ "identifier": "ca",
+ "name": "Catalan",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-ca.vtt"
+ },
+ {
+ "identifier": "cs",
+ "name": "Czech",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-cs.vtt"
+ },
+ {
+ "identifier": "de",
+ "name": "German",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-de.vtt"
+ },
+ {
+ "identifier": "en",
+ "name": "English",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-en.vtt"
+ },
+ {
+ "identifier": "es",
+ "name": "Spanish",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-es.vtt"
+ },
+ {
+ "identifier": "eu",
+ "name": "Basque",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-eu.vtt"
+ },
+ {
+ "identifier": "fr",
+ "name": "French",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-fr.vtt"
+ },
+ {
+ "identifier": "gl",
+ "name": "Galician",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-gl.vtt"
+ },
+ {
+ "identifier": "hu",
+ "name": "Hungarian",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-hu.vtt"
+ },
+ {
+ "identifier": "it",
+ "name": "Italian",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-it.vtt"
+ },
+ {
+ "identifier": "lt",
+ "name": "Lithuanian",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-lt.vtt"
+ },
+ {
+ "identifier": "nl",
+ "name": "Dutch",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-nl.vtt"
+ },
+ {
+ "identifier": "pl",
+ "name": "Polish",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-pl.vtt"
+ },
+ {
+ "identifier": "pt",
+ "name": "Portuguese",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-pt.vtt"
+ },
+ {
+ "identifier": "ru",
+ "name": "Russian",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-ru.vtt"
+ },
+ {
+ "identifier": "sv",
+ "name": "Swedish",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-sv.vtt"
+ },
+ {
+ "identifier": "zh",
+ "name": "Chinese",
+ "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-zh.vtt"
+ }
+ ],
+ "icon": [
+ {
+ "type": "Image",
+ "url": "https://framatube.org/static/thumbnails/9c9de5e8-0a1e-484a-b099-e80766180a6d.jpg",
+ "mediaType": "image/jpeg",
+ "width": 223,
+ "height": 122
+ },
+ {
+ "type": "Image",
+ "url": "https://framatube.org/static/previews/9c9de5e8-0a1e-484a-b099-e80766180a6d.jpg",
+ "mediaType": "image/jpeg",
+ "width": 850,
+ "height": 480
+ }
+ ],
+ "url": [
+ {
+ "type": "Link",
+ "mediaType": "text/html",
+ "href": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d"
+ },
+ {
+ "type": "Link",
+ "mediaType": "video/mp4",
+ "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4",
+ "height": 1080,
+ "size": 14689568,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "rel": ["metadata", "video/mp4"],
+ "mediaType": "application/json",
+ "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10124",
+ "height": 1080,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent",
+ "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.torrent",
+ "height": 1080
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.torrent&xt=urn:btih:dc84b692c4002fec0cae873df0dc7f5d67fc09db&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4",
+ "height": 1080
+ },
+ {
+ "type": "Link",
+ "mediaType": "video/mp4",
+ "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4",
+ "height": 720,
+ "size": 8365049,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "rel": ["metadata", "video/mp4"],
+ "mediaType": "application/json",
+ "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10127",
+ "height": 720,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent",
+ "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-720.torrent",
+ "height": 720
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.torrent&xt=urn:btih:9fb0e35b4945565fa842b2bb0fe8b03edf223b15&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4",
+ "height": 720
+ },
+ {
+ "type": "Link",
+ "mediaType": "video/mp4",
+ "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4",
+ "height": 480,
+ "size": 5650553,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "rel": ["metadata", "video/mp4"],
+ "mediaType": "application/json",
+ "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10125",
+ "height": 480,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent",
+ "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-480.torrent",
+ "height": 480
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.torrent&xt=urn:btih:bf02027a9aad4275d8baa25afae230b85187bcf7&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4",
+ "height": 480
+ },
+ {
+ "type": "Link",
+ "mediaType": "video/mp4",
+ "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4",
+ "height": 360,
+ "size": 4524751,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "rel": ["metadata", "video/mp4"],
+ "mediaType": "application/json",
+ "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10126",
+ "height": 360,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent",
+ "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-360.torrent",
+ "height": 360
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.torrent&xt=urn:btih:16b3720b18752523e2848341d3207120f2de26f8&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4",
+ "height": 360
+ },
+ {
+ "type": "Link",
+ "mediaType": "video/mp4",
+ "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4",
+ "height": 240,
+ "size": 3452697,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "rel": ["metadata", "video/mp4"],
+ "mediaType": "application/json",
+ "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10128",
+ "height": 240,
+ "fps": 24
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent",
+ "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent",
+ "height": 240
+ },
+ {
+ "type": "Link",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent&xt=urn:btih:38b4747ff788b30bf61f59d1965cd38f9e48e01f&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4",
+ "height": 240
+ }
+ ],
+ "likes": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/likes",
+ "dislikes": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/dislikes",
+ "shares": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/announces",
+ "comments": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/comments",
+ "attributedTo": [
+ {
+ "type": "Person",
+ "id": "https://framatube.org/accounts/framasoft"
+ },
+ {
+ "type": "Group",
+ "id": "https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8"
+ }
+ ],
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": ["https://framatube.org/accounts/framasoft/followers"],
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+ },
+ {
+ "pt": "https://joinpeertube.org/ns#",
+ "sc": "http://schema.org#",
+ "Hashtag": "as:Hashtag",
+ "uuid": "sc:identifier",
+ "category": "sc:category",
+ "licence": "sc:license",
+ "subtitleLanguage": "sc:subtitleLanguage",
+ "sensitive": "as:sensitive",
+ "language": "sc:inLanguage",
+ "Infohash": "pt:Infohash",
+ "Playlist": "pt:Playlist",
+ "PlaylistElement": "pt:PlaylistElement",
+ "originallyPublishedAt": "sc:datePublished",
+ "views": {
+ "@type": "sc:Number",
+ "@id": "pt:views"
+ },
+ "state": {
+ "@type": "sc:Number",
+ "@id": "pt:state"
+ },
+ "size": {
+ "@type": "sc:Number",
+ "@id": "pt:size"
+ },
+ "fps": {
+ "@type": "sc:Number",
+ "@id": "pt:fps"
+ },
+ "startTimestamp": {
+ "@type": "sc:Number",
+ "@id": "pt:startTimestamp"
+ },
+ "stopTimestamp": {
+ "@type": "sc:Number",
+ "@id": "pt:stopTimestamp"
+ },
+ "position": {
+ "@type": "sc:Number",
+ "@id": "pt:position"
+ },
+ "commentsEnabled": {
+ "@type": "sc:Boolean",
+ "@id": "pt:commentsEnabled"
+ },
+ "downloadEnabled": {
+ "@type": "sc:Boolean",
+ "@id": "pt:downloadEnabled"
+ },
+ "waitTranscoding": {
+ "@type": "sc:Boolean",
+ "@id": "pt:waitTranscoding"
+ },
+ "support": {
+ "@type": "sc:Text",
+ "@id": "pt:support"
+ },
+ "likes": {
+ "@id": "as:likes",
+ "@type": "@id"
+ },
+ "dislikes": {
+ "@id": "as:dislikes",
+ "@type": "@id"
+ },
+ "playlists": {
+ "@id": "pt:playlists",
+ "@type": "@id"
+ },
+ "shares": {
+ "@id": "as:shares",
+ "@type": "@id"
+ },
+ "comments": {
+ "@id": "as:comments",
+ "@type": "@id"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/pleroma-comment-object.json b/test/fixtures/pleroma-comment-object.json
new file mode 100644
index 00000000..843170f5
--- /dev/null
+++ b/test/fixtures/pleroma-comment-object.json
@@ -0,0 +1,30 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://blob.cat/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "actor": "https://blob.cat/users/comicbot",
+ "attachment": [
+ {
+ "mediaType": "image/gif",
+ "name": "1574936800141.gif",
+ "type": "Document",
+ "url": "https://blob.cat/media/143ba9b1ed15e67d7401906f7b71a459b90680af7075af5b8ac9cb8e3b86868a.gif"
+ }
+ ],
+ "attributedTo": "https://blob.cat/users/comicbot",
+ "cc": ["https://blob.cat/users/comicbot/followers"],
+ "content": "Super Mega Comics
http://supermegacomics.com/",
+ "context": "https://blob.cat/contexts/26f3271a-3eb8-4f3f-8fb1-8ff96e2c4a47",
+ "conversation": "https://blob.cat/contexts/26f3271a-3eb8-4f3f-8fb1-8ff96e2c4a47",
+ "id": "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94",
+ "published": "2019-11-28T10:26:42.503473Z",
+ "sensitive": false,
+ "summary": "",
+ "tag": [],
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "type": "Note"
+}
diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json b/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
deleted file mode 100644
index 0c696bf2..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
+++ /dev/null
@@ -1,31 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": {
- "Accept": "*/*",
- "Accept-Encoding":"deflate, gzip"
- },
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718"
- },
- "response": {
- "binary": false,
- "body": "{\"type\": \"FeatureCollection\", \"version\": \"draft\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [4.842569, 45.751718]}, \"properties\": {\"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.9999999999926557, \"housenumber\": \"10\", \"id\": \"69387_3650_00010\", \"type\": \"housenumber\", \"x\": 843232.29, \"y\": 6518573.31, \"importance\": 0.5454797306366062, \"name\": \"10 Rue Jangot\", \"postcode\": \"69007\", \"citycode\": \"69387\", \"city\": \"Lyon\", \"district\": \"Lyon 7e Arrondissement\", \"context\": \"69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes\", \"street\": \"Rue Jangot\", \"distance\": 0}}], \"attribution\": \"BAN\", \"licence\": \"ETALAB-2.0\", \"limit\": 1}",
- "headers": {
- "Server": "nginx/1.9.3",
- "Date": "Thu, 14 Mar 2019 10:46:45 GMT",
- "Content-type":"application/json; charset=utf-8",
- "Vary":"Accept-Encoding",
- "X-cache-status":"MISS",
- "Access-control-allow-origin":"*",
- "Access-control-allow-headers":"X-Requested-With,Content-Type",
- "Content-encoding":"gzip"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/search.json b/test/fixtures/vcr_cassettes/geospatial/addok/search.json
deleted file mode 100644
index d471d695..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/addok/search.json
+++ /dev/null
@@ -1,31 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": {
- "Accept": "*/*",
- "Accept-Encoding":"deflate, gzip"
- },
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://api-adresse.data.gouv.fr/search/?q=10%20Rue%20Jangot"
- },
- "response": {
- "binary": false,
- "body": "{\"type\": \"FeatureCollection\", \"version\": \"draft\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [4.842569, 45.751718]}, \"properties\": {\"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.8677708846033279, \"housenumber\": \"10\", \"id\": \"69387_3650_00010\", \"type\": \"housenumber\", \"x\": 843232.29, \"y\": 6518573.31, \"importance\": 0.5454797306366062, \"name\": \"10 Rue Jangot\", \"postcode\": \"69007\", \"citycode\": \"69387\", \"city\": \"Lyon\", \"district\": \"Lyon 7e Arrondissement\", \"context\": \"69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes\", \"street\": \"Rue Jangot\"}}, {\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [2.440319, 50.371266]}, \"properties\": {\"label\": \"Rue Jangon 62127 Bailleul-aux-Cornailles\", \"score\": 0.5269641371131077, \"id\": \"62070_0100\", \"type\": \"street\", \"x\": 660129.18, \"y\": 7030540.46, \"importance\": 0.25814396978264664, \"name\": \"Rue Jangon\", \"postcode\": \"62127\", \"citycode\": \"62070\", \"city\": \"Bailleul-aux-Cornailles\", \"context\": \"62, Pas-de-Calais, Hauts-de-France\"}}], \"attribution\": \"BAN\", \"licence\": \"ETALAB-2.0\", \"query\": \"10 Rue Jangot\", \"limit\": 5}",
- "headers": {
- "Server":"nginx/1.10.3",
- "Date":"Thu, 25 Jun 2020 11:23:54 GMT",
- "Content-type":"application/json; charset=utf-8",
- "Vary":"Accept-Encoding",
- "X-cache-status":"MISS",
- "Access-control-allow-origin":"*",
- "Access-control-allow-headers":"X-Requested-With,Content-Type",
- "Content-encoding":"gzip"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
deleted file mode 100644
index 26bbccc4..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
+++ /dev/null
@@ -1,63 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://maps.googleapis.com/maps/api/place/details/json?key=toto&placeid=ChIJrW0QikTq9EcR96jk2OnO75w"
- },
- "response": {
- "binary": false,
- "body": "{\n \"html_attributions\" : [],\n \"result\" : {\n \"address_components\" : [\n {\n \"long_name\" : \"10bis\",\n \"short_name\" : \"10bis\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"adr_address\" : \"\\u003cspan class=\\\"street-address\\\"\\u003e10bis Rue Jangot\\u003c/span\\u003e, \\u003cspan class=\\\"postal-code\\\"\\u003e69007\\u003c/span\\u003e \\u003cspan class=\\\"locality\\\"\\u003eLyon\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eFrance\\u003c/span\\u003e\",\n \"formatted_address\" : \"10bis Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.751725,\n \"lng\" : 4.8424966\n },\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7531097802915,\n \"lng\" : 4.843951380291502\n },\n \"southwest\" : {\n \"lat\" : 45.7504118197085,\n \"lng\" : 4.841253419708497\n }\n }\n },\n \"icon\" : \"https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png\",\n \"id\" : \"4a3482a7a74c6203048adf713b736186c4ace7cd\",\n \"name\" : \"10bis Rue Jangot\",\n \"place_id\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"reference\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"scope\" : \"GOOGLE\",\n \"types\" : [ \"street_address\" ],\n \"url\" : \"https://maps.google.com/?q=10bis+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106dad:0x9cefcee9d8e4a8f7\",\n \"utc_offset\" : 120,\n \"vicinity\" : \"Lyon\"\n },\n \"status\" : \"OK\"\n}\n",
- "headers": {
- "Content-Type": "application/json; charset=UTF-8",
- "Date": "Thu, 22 Aug 2019 13:18:50 GMT",
- "Expires": "Thu, 22 Aug 2019 13:23:50 GMT",
- "Cache-Control": "public, max-age=300",
- "Server": "scaffolding on HTTPServer2",
- "X-XSS-Protection": "0",
- "X-Frame-Options": "SAMEORIGIN",
- "Server-Timing": "gfet4t7; dur=87",
- "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"",
- "Accept-Ranges": "none",
- "Vary": "Accept-Language,Accept-Encoding",
- "Transfer-Encoding": "chunked"
- },
- "status_code": 200,
- "type": "ok"
- }
- },
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&latlng=45.751718,4.842569&result_type=street_address"
- },
- "response": {
- "binary": false,
- "body": "{\n \"plus_code\" : {\n \"compound_code\" : \"QR2V+M2 Lyon, France\",\n \"global_code\" : \"8FQ6QR2V+M2\"\n },\n \"results\" : [\n {\n \"address_components\" : [\n {\n \"long_name\" : \"10bis\",\n \"short_name\" : \"10bis\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"10bis Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.751725,\n \"lng\" : 4.8424966\n },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7530739802915,\n \"lng\" : 4.843845580291503\n },\n \"southwest\" : {\n \"lat\" : 45.7503760197085,\n \"lng\" : 4.841147619708499\n }\n }\n },\n \"place_id\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"types\" : [ \"street_address\" ]\n },\n {\n \"address_components\" : [\n {\n \"long_name\" : \"9\",\n \"short_name\" : \"9\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"9 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.7518165,\n \"lng\" : 4.8427168\n },\n \"location_type\" : \"RANGE_INTERPOLATED\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7531654802915,\n \"lng\" : 4.844065780291502\n },\n \"southwest\" : {\n \"lat\" : 45.7504675197085,\n \"lng\" : 4.841367819708497\n }\n }\n },\n \"place_id\" : \"EiA5IFJ1ZSBKYW5nb3QsIDY5MDA3IEx5b24sIEZyYW5jZSIaEhgKFAoSCR8N2ItE6vRHEW9tyPnhQsUIEAk\",\n \"types\" : [ \"street_address\" ]\n }\n ],\n \"status\" : \"OK\"\n}\n",
- "headers": {
- "Content-Type": "application/json; charset=UTF-8",
- "Date": "Thu, 22 Aug 2019 13:18:50 GMT",
- "Expires": "Thu, 22 Aug 2019 13:19:20 GMT",
- "Cache-Control": "public, max-age=30",
- "Access-Control-Allow-Origin": "*",
- "Server": "mafe",
- "X-XSS-Protection": "0",
- "X-Frame-Options": "SAMEORIGIN",
- "Server-Timing": "gfet4t7; dur=30",
- "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"",
- "Accept-Ranges": "none",
- "Vary": "Accept-Encoding",
- "Transfer-Encoding": "chunked"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
deleted file mode 100644
index f06de36b..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
+++ /dev/null
@@ -1,63 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&address=10%20rue%20Jangot"
- },
- "response": {
- "binary": false,
- "body": "{\n \"results\" : [\n {\n \"address_components\" : [\n {\n \"long_name\" : \"10\",\n \"short_name\" : \"10\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.75164940000001,\n \"lng\" : 4.8424032\n },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.75299838029151,\n \"lng\" : 4.843752180291502\n },\n \"southwest\" : {\n \"lat\" : 45.75030041970851,\n \"lng\" : 4.841054219708497\n }\n }\n },\n \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"types\" : [ \"street_address\" ]\n }\n ],\n \"status\" : \"OK\"\n}\n",
- "headers": {
- "Content-Type": "application/json; charset=UTF-8",
- "Date": "Thu, 22 Aug 2019 13:05:39 GMT",
- "Expires": "Thu, 22 Aug 2019 13:06:09 GMT",
- "Cache-Control": "public, max-age=30",
- "Access-Control-Allow-Origin": "*",
- "Server": "mafe",
- "X-XSS-Protection": "0",
- "X-Frame-Options": "SAMEORIGIN",
- "Server-Timing": "gfet4t7; dur=44",
- "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"",
- "Accept-Ranges": "none",
- "Vary": "Accept-Encoding",
- "Transfer-Encoding": "chunked"
- },
- "status_code": 200,
- "type": "ok"
- }
- },
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://maps.googleapis.com/maps/api/place/details/json?key=toto&placeid=ChIJtW0QikTq9EcRLI4Vy6bRx0U"
- },
- "response": {
- "binary": false,
- "body": "{\n \"html_attributions\" : [],\n \"result\" : {\n \"address_components\" : [\n {\n \"long_name\" : \"10\",\n \"short_name\" : \"10\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"adr_address\" : \"\\u003cspan class=\\\"street-address\\\"\\u003e10 Rue Jangot\\u003c/span\\u003e, \\u003cspan class=\\\"postal-code\\\"\\u003e69007\\u003c/span\\u003e \\u003cspan class=\\\"locality\\\"\\u003eLyon\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eFrance\\u003c/span\\u003e\",\n \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.75164940000001,\n \"lng\" : 4.842403200000001\n },\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7530412802915,\n \"lng\" : 4.843668630291503\n },\n \"southwest\" : {\n \"lat\" : 45.75034331970851,\n \"lng\" : 4.840970669708498\n }\n }\n },\n \"icon\" : \"https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png\",\n \"id\" : \"61b9418d092d2ed05ddd65a55dddefda5b9628cc\",\n \"name\" : \"10 Rue Jangot\",\n \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"reference\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"scope\" : \"GOOGLE\",\n \"types\" : [ \"street_address\" ],\n \"url\" : \"https://maps.google.com/?q=10+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106db5:0x45c7d1a6cb158e2c\",\n \"utc_offset\" : 120,\n \"vicinity\" : \"Lyon\"\n },\n \"status\" : \"OK\"\n}\n",
- "headers": {
- "Content-Type": "application/json; charset=UTF-8",
- "Date": "Thu, 22 Aug 2019 13:05:39 GMT",
- "Expires": "Thu, 22 Aug 2019 13:10:39 GMT",
- "Cache-Control": "public, max-age=300",
- "Server": "scaffolding on HTTPServer2",
- "X-XSS-Protection": "0",
- "X-Frame-Options": "SAMEORIGIN",
- "Server-Timing": "gfet4t7; dur=86",
- "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"",
- "Accept-Ranges": "none",
- "Vary": "Accept-Language,Accept-Encoding",
- "Transfer-Encoding": "chunked"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
deleted file mode 100644
index a42c62f0..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
+++ /dev/null
@@ -1,36 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10"
- },
- "response": {
- "binary": false,
- "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":1,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"latLng\":{\"lat\":45.751718,\"lng\":4.842569}},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AAA\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433\"}]}]}",
- "headers": {
- "Access-Control-Allow-Methods": "OPTIONS,GET,POST",
- "Access-Control-Allow-Origin": "*",
- "Cache-Control": "no-cache, must-revalidate",
- "Content-Type": "application/json;charset=UTF-8",
- "Date": "Thu, 14 Mar 2019 09:27:01 GMT",
- "Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
- "GeocodeTransactionCount": "0",
- "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
- "Pragma": "no-cache",
- "ReverseGeocodeTransactionCount": "1",
- "Server": "Apache-Coyote/1.1",
- "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
- "status": "success",
- "transactionWeight": "1.0",
- "Content-Length": "1063",
- "Connection": "keep-alive"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
deleted file mode 100644
index 4b23ed25..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
+++ /dev/null
@@ -1,36 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10"
- },
- "response": {
- "binary": false,
- "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":10,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"location\":\"10 rue Jangot\"},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"7e\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"Lyon\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AXX\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752\"}]}]}",
- "headers": {
- "Access-Control-Allow-Methods": "OPTIONS,GET,POST",
- "Access-Control-Allow-Origin": "*",
- "Cache-Control": "no-cache, must-revalidate",
- "Content-Type": "application/json;charset=UTF-8",
- "Date": "Thu, 14 Mar 2019 09:27:01 GMT",
- "Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
- "GeocodeTransactionCount": "1",
- "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
- "Pragma": "no-cache",
- "ReverseGeocodeTransactionCount": "0",
- "Server": "Apache-Coyote/1.1",
- "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
- "status": "success",
- "transactionWeight": "1.0",
- "Content-Length": "1055",
- "Connection": "keep-alive"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
deleted file mode 100644
index ad9beef8..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
+++ /dev/null
@@ -1,32 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": {
- "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
- },
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=1"
- },
- "response": {
- "binary": false,
- "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"45.751718,4.842569\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"accuracy\":0,\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
- "headers": {
- "Date": "Tue, 12 Nov 2019 12:21:45 GMT",
- "Server": "Apache/2.4.29 (Ubuntu)",
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Methods": "OPTIONS,GET",
- "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
- "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
- "Upgrade": "h2",
- "Connection": "Upgrade, close",
- "Transfer-Encoding": "chunked",
- "Content-Type": "application/json; charset=UTF-8"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
deleted file mode 100644
index 395cf11e..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
+++ /dev/null
@@ -1,32 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": {
- "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
- },
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=1"
- },
- "response": {
- "binary": false,
- "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"10 rue Jangot\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
- "headers": {
- "Date": "Tue, 12 Nov 2019 12:21:46 GMT",
- "Server": "Apache/2.4.29 (Ubuntu)",
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Methods": "OPTIONS,GET",
- "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
- "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
- "Upgrade": "h2",
- "Connection": "Upgrade, close",
- "Transfer-Encoding": "chunked",
- "Content-Type": "application/json; charset=UTF-8"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json b/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
deleted file mode 100644
index 8ff49672..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://photon.komoot.de/reverse?lon=4.842569&lat=45.751718"
- },
- "response": {
- "binary": false,
- "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8416864,45.7605435],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":4662865602,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"leisure\",\"city\":\"Lyon\",\"street\":\"Rue Pravaz\",\"osm_value\":\"fitness_centre\",\"postcode\":\"69003\",\"name\":\"L'appart Fitness\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
- "headers": {
- "Server": "nginx/1.9.3 (Ubuntu)",
- "Date": "Thu, 14 Mar 2019 10:46:45 GMT",
- "Content-Type": "application/json;charset=utf-8",
- "Transfer-Encoding": "chunked",
- "Connection": "keep-alive",
- "Access-Control-Allow-Origin": "*"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/search.json b/test/fixtures/vcr_cassettes/geospatial/photon/search.json
deleted file mode 100644
index 45a644e6..00000000
--- a/test/fixtures/vcr_cassettes/geospatial/photon/search.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "request": {
- "body": "",
- "headers": [],
- "method": "get",
- "options": [],
- "request_body": "",
- "url": "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10"
- },
- "response": {
- "binary": false,
- "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8425657,45.7517141],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260611,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}},{\"geometry\":{\"coordinates\":[4.8424254,45.7517056],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260612,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10bis\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
- "headers": {
- "Server": "nginx/1.9.3 (Ubuntu)",
- "Date": "Thu, 14 Mar 2019 10:46:43 GMT",
- "Content-Type": "application/json;charset=utf-8",
- "Transfer-Encoding": "chunked",
- "Connection": "keep-alive",
- "Access-Control-Allow-Origin": "*"
- },
- "status_code": 200,
- "type": "ok"
- }
- }
-]
\ No newline at end of file
diff --git a/test/graphql/api/report_test.exs b/test/graphql/api/report_test.exs
index 5b0c85fb..fbf4e377 100644
--- a/test/graphql/api/report_test.exs
+++ b/test/graphql/api/report_test.exs
@@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do
import Mobilizon.Factory
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Formatter.HTML
diff --git a/test/graphql/resolvers/post_test.exs b/test/graphql/resolvers/post_test.exs
new file mode 100644
index 00000000..ddc365eb
--- /dev/null
+++ b/test/graphql/resolvers/post_test.exs
@@ -0,0 +1,627 @@
+defmodule Mobilizon.GraphQL.Resolvers.PostTest do
+ use Mobilizon.Web.ConnCase
+
+ import Mobilizon.Factory
+
+ alias Mobilizon.Actors.{Actor, Member}
+ alias Mobilizon.Posts.Post
+ alias Mobilizon.Users.User
+
+ alias Mobilizon.GraphQL.AbsintheHelpers
+
+ @post_fragment """
+ fragment PostFragment on Post {
+ id
+ title
+ slug
+ url
+ body
+ author {
+ id
+ preferredUsername
+ name
+ avatar {
+ url
+ }
+ }
+ attributedTo {
+ id
+ preferredUsername
+ name
+ avatar {
+ url
+ }
+ }
+ visibility
+ insertedAt
+ updatedAt
+ draft
+ }
+ """
+
+ @get_group_posts """
+ query($name: String!, $page: Int, $limit: Int) {
+ group(preferredUsername: $name) {
+ id
+ posts(page: $page, limit: $limit) {
+ elements {
+ id,
+ title,
+ },
+ total
+ },
+ }
+ }
+ """
+
+ @post_query """
+ query Post($slug: String!) {
+ post(slug: $slug) {
+ ...PostFragment
+ }
+ }
+ #{@post_fragment}
+ """
+
+ @create_post """
+ mutation CreatePost($title: String!, $body: String, $attributedToId: ID!, $draft: Boolean) {
+ createPost(title: $title, body: $body, attributedToId: $attributedToId, draft: $draft) {
+ ...PostFragment
+ }
+ }
+ #{@post_fragment}
+ """
+
+ @update_post """
+ mutation UpdatePost($id: ID!, $title: String, $body: String, $attributedToId: ID, $draft: Boolean) {
+ updatePost(id: $id, title: $title, body: $body, attributedToId: $attributedToId, draft: $draft) {
+ ...PostFragment
+ }
+ }
+ #{@post_fragment}
+ """
+
+ @delete_post """
+ mutation DeletePost($id: ID!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+ """
+
+ @post_title "my post"
+ @updated_post_title "my updated post"
+
+ setup do
+ %User{} = user = insert(:user)
+ %Actor{} = actor = insert(:actor, user: user)
+ %Actor{} = group = insert(:group)
+ %Post{} = post = insert(:post, attributed_to: group, author: actor)
+
+ %Post{} =
+ post_unlisted = insert(:post, attributed_to: group, author: actor, visibility: :unlisted)
+
+ %Post{} = post_draft = insert(:post, attributed_to: group, author: actor, draft: true)
+ %Member{} = insert(:member, parent: group, actor: actor, role: :member)
+
+ %Post{} =
+ post_private = insert(:post, attributed_to: group, author: actor, visibility: :private)
+
+ {:ok,
+ user: user,
+ group: group,
+ post: post,
+ post_unlisted: post_unlisted,
+ post_draft: post_draft,
+ post_private: post_private}
+ end
+
+ describe "Resolver: Get group's posts" do
+ test "find_posts_for_group/3", %{
+ conn: conn,
+ user: user,
+ group: group,
+ post: post,
+ post_unlisted: post_unlisted,
+ post_draft: post_draft,
+ post_private: post_private
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @get_group_posts,
+ variables: %{
+ name: group.preferred_username
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["group"]["posts"]["total"] == 4
+
+ assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) |> MapSet.new() ==
+ MapSet.new([
+ post.id,
+ post_unlisted.id,
+ post_draft.id,
+ post_private.id
+ ])
+ end
+
+ test "find_posts_for_group/3 when not member of group", %{
+ conn: conn,
+ group: group,
+ post: post
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @get_group_posts,
+ variables: %{
+ name: group.preferred_username
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["group"]["posts"]["total"] == 1
+
+ assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) == [post.id]
+ end
+
+ test "find_posts_for_group/3 when not connected", %{
+ conn: conn,
+ group: group,
+ post: post
+ } do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @get_group_posts,
+ variables: %{
+ name: group.preferred_username
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["group"]["posts"]["total"] == 1
+
+ assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) == [post.id]
+ end
+ end
+
+ describe "Resolver: Get a specific post" do
+ test "get_post/3 for a public post", %{
+ conn: conn,
+ user: user,
+ post: post
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post.title
+ end
+
+ test "get_post/3 for a non-existing post", %{
+ conn: conn,
+ user: user
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: "not existing"
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+
+ test "get_post/3 for an unlisted post", %{
+ conn: conn,
+ user: user,
+ post_unlisted: post_unlisted
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_unlisted.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post_unlisted.title
+
+ assert res["data"]["post"]["visibility"] ==
+ post_unlisted.visibility |> to_string() |> String.upcase()
+ end
+
+ test "get_post/3 for a private post", %{
+ conn: conn,
+ user: user,
+ post_private: post_private
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_private.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post_private.title
+
+ assert res["data"]["post"]["visibility"] ==
+ post_private.visibility |> to_string() |> String.upcase()
+ end
+
+ test "get_post/3 for a draft post", %{
+ conn: conn,
+ user: user,
+ post_draft: post_draft
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_draft.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post_draft.title
+ assert res["data"]["post"]["draft"] == true
+ end
+
+ test "get_post/3 without being a member for a public post", %{
+ conn: conn,
+ post: post
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post.title
+ end
+
+ test "get_post/3 without being connected for a public post", %{
+ conn: conn,
+ post: post
+ } do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post.title
+ end
+
+ test "get_post/3 without being a member for an unlisted post", %{
+ conn: conn,
+ post_unlisted: post_unlisted
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_unlisted.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post_unlisted.title
+
+ assert res["data"]["post"]["visibility"] ==
+ post_unlisted.visibility |> to_string() |> String.upcase()
+ end
+
+ test "get_post/3 without being a member for a private post", %{
+ conn: conn,
+ post_private: post_private
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_private.slug
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+
+ test "get_post/3 without being connected for an unlisted post still gives the post", %{
+ conn: conn,
+ post_unlisted: post_unlisted
+ } do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_unlisted.slug
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["post"]["title"] == post_unlisted.title
+
+ assert res["data"]["post"]["visibility"] ==
+ post_unlisted.visibility |> to_string() |> String.upcase()
+ end
+
+ test "get_post/3 without being connected for a private post", %{
+ conn: conn,
+ post_private: post_private
+ } do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_private.slug
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+
+ test "get_post/3 without being a member for a draft post", %{
+ conn: conn,
+ post_draft: post_draft
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_draft.slug
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+
+ test "get_post/3 without being connected for a draft post", %{
+ conn: conn,
+ post_draft: post_draft
+ } do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_draft.slug
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+ end
+
+ describe "Resolver: Create a post" do
+ test "create_post/3 creates a post for a group", %{
+ conn: conn,
+ user: user,
+ group: group
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @create_post,
+ variables: %{
+ title: @post_title,
+ body: "My new post is here",
+ attributedToId: group.id
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["createPost"]["title"] == @post_title
+ id = res["data"]["createPost"]["id"]
+ assert res["data"]["createPost"]["slug"] == "my-post-#{ShortUUID.encode!(id)}"
+ end
+
+ test "create_post/3 doesn't create a post if no group is defined", %{
+ conn: conn,
+ user: user
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @create_post,
+ variables: %{
+ title: @post_title,
+ body: "some body",
+ attributedToId: nil
+ }
+ )
+
+ assert Enum.map(res["errors"], & &1["message"]) == [
+ "Argument \"attributedToId\" has invalid value $attributedToId.",
+ "Variable \"attributedToId\": Expected non-null, found null."
+ ]
+ end
+
+ test "create_post/3 doesn't create a post if the actor is not a member of the group",
+ %{
+ conn: conn,
+ group: group
+ } do
+ %User{} = user = insert(:user)
+ %Actor{} = insert(:actor, user: user)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @create_post,
+ variables: %{
+ title: @post_title,
+ body: "My body",
+ attributedToId: group.id
+ }
+ )
+
+ assert Enum.map(res["errors"], & &1["message"]) == [
+ "Actor id is not member of group"
+ ]
+ end
+ end
+
+ describe "Resolver: Update a post" do
+ test "update_post/3 updates a post for a group", %{
+ conn: conn,
+ user: user,
+ group: group
+ } do
+ %Post{id: post_id} = insert(:post, attributed_to: group)
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @update_post,
+ variables: %{
+ id: post_id,
+ title: @updated_post_title
+ }
+ )
+
+ assert is_nil(res["errors"])
+
+ assert res["data"]["updatePost"]["title"] == @updated_post_title
+ end
+ end
+
+ describe "Resolver: Delete a post" do
+ test "delete_post/3 deletes a post", %{
+ conn: conn,
+ user: user,
+ group: group
+ } do
+ %Post{id: post_id, slug: post_slug} =
+ insert(:post,
+ attributed_to: group
+ )
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @delete_post,
+ variables: %{
+ id: post_id
+ }
+ )
+
+ assert is_nil(res["errors"])
+ assert res["data"]["deletePost"]["id"] == post_id
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @post_query,
+ variables: %{
+ slug: post_slug
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "No such post"
+ end
+
+ test "delete_post/3 deletes a post not found", %{
+ conn: conn,
+ user: user
+ } do
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @delete_post,
+ variables: %{
+ id: "not found"
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "Post ID is not a valid ID"
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(
+ query: @delete_post,
+ variables: %{
+ id: "d276ef98-8433-48d7-890e-c24eda0dcdbe"
+ }
+ )
+
+ assert hd(res["errors"])["message"] == "Post doesn't exist"
+ end
+ end
+end
diff --git a/test/graphql/resolvers/user_test.exs b/test/graphql/resolvers/user_test.exs
index 477e9fed..70637a32 100644
--- a/test/graphql/resolvers/user_test.exs
+++ b/test/graphql/resolvers/user_test.exs
@@ -5,9 +5,9 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
import Mobilizon.Factory
- alias Mobilizon.{Actors, Config, Conversations, Events, Users}
+ alias Mobilizon.{Actors, Config, Discussions, Events, Users}
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Users.User
@@ -1388,7 +1388,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
end
assert_raise Ecto.NoResultsError, fn ->
- Conversations.get_comment!(comment_id)
+ Discussions.get_comment!(comment_id)
end
# Actors are not deleted but emptied (to keep the username reserved)
diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs
index 8b863df2..fd6ed2f7 100644
--- a/test/mobilizon/actors/actors_test.exs
+++ b/test/mobilizon/actors/actors_test.exs
@@ -5,9 +5,9 @@ defmodule Mobilizon.ActorsTest do
import Mobilizon.Factory
- alias Mobilizon.{Actors, Config, Conversations, Events, Tombstone, Users}
+ alias Mobilizon.{Actors, Config, Discussions, Events, Tombstone, Users}
alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
- alias Mobilizon.Conversations.Comment
+ alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Media.File, as: FileModel
alias Mobilizon.Service.Workers
@@ -291,6 +291,56 @@ defmodule Mobilizon.ActorsTest do
assert actor = actor_fetched
end
+ test "perform delete the actor actually deletes the actor", %{
+ actor: %Actor{avatar: %{url: avatar_url}, banner: %{url: banner_url}, id: actor_id} = actor
+ } do
+ %Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
+ insert(:event, organizer_actor: actor)
+
+ %Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor)
+ insert(:comment, actor: actor)
+
+ %URI{path: "/media/" <> avatar_path} = URI.parse(avatar_url)
+ %URI{path: "/media/" <> banner_path} = URI.parse(banner_url)
+
+ assert File.exists?(
+ Config.get!([Uploader.Local, :uploads]) <>
+ "/" <> avatar_path
+ )
+
+ assert File.exists?(
+ Config.get!([Uploader.Local, :uploads]) <>
+ "/" <> banner_path
+ )
+
+ assert {:ok, %Actor{}} = Actors.perform(:delete_actor, actor)
+
+ assert %Actor{
+ name: nil,
+ summary: nil,
+ suspended: true,
+ avatar: nil,
+ banner: nil,
+ user_id: nil
+ } = Actors.get_actor(actor_id)
+
+ assert {:error, :event_not_found} = Events.get_event(event1.id)
+ assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
+ assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
+ refute is_nil(deleted_at)
+ assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
+
+ refute File.exists?(
+ Config.get!([Uploader.Local, :uploads]) <>
+ "/" <> avatar_path
+ )
+
+ refute File.exists?(
+ Config.get!([Uploader.Local, :uploads]) <>
+ "/" <> banner_path
+ )
+ end
+
test "delete_actor/1 deletes the actor", %{
actor: %Actor{avatar: %{url: avatar_url}, banner: %{url: banner_url}, id: actor_id} = actor
} do
@@ -333,7 +383,7 @@ defmodule Mobilizon.ActorsTest do
assert {:error, :event_not_found} = Events.get_event(event1.id)
assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
- assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id)
+ assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
refute is_nil(deleted_at)
assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
diff --git a/test/mobilizon/conversations_test.exs b/test/mobilizon/discussions_test.exs
similarity index 65%
rename from test/mobilizon/conversations_test.exs
rename to test/mobilizon/discussions_test.exs
index bd95205d..5e9984e0 100644
--- a/test/mobilizon/conversations_test.exs
+++ b/test/mobilizon/discussions_test.exs
@@ -1,13 +1,11 @@
-defmodule Mobilizon.ConversationsTest do
+defmodule Mobilizon.DiscussionsTest do
use Mobilizon.DataCase
import Mobilizon.Factory
alias Mobilizon.Actors.Actor
- alias Mobilizon.Conversations
- alias Mobilizon.Conversations.Comment
- alias Mobilizon.Service.Workers
- alias Mobilizon.Storage.Page
+ alias Mobilizon.Discussions
+ alias Mobilizon.Discussions.Comment
describe "comments" do
@valid_attrs %{text: "some text"}
@@ -16,13 +14,13 @@ defmodule Mobilizon.ConversationsTest do
test "list_comments/0 returns all comments" do
%Comment{id: comment_id} = insert(:comment)
- comment_ids = Conversations.list_comments() |> Enum.map(& &1.id)
+ comment_ids = Discussions.list_comments() |> Enum.map(& &1.id)
assert comment_ids == [comment_id]
end
test "get_comment!/1 returns the comment with given id" do
%Comment{id: comment_id} = insert(:comment)
- comment_fetched = Conversations.get_comment!(comment_id)
+ comment_fetched = Discussions.get_comment!(comment_id)
assert comment_fetched.id == comment_id
end
@@ -30,7 +28,7 @@ defmodule Mobilizon.ConversationsTest do
%Actor{} = actor = insert(:actor)
comment_data = Map.merge(@valid_attrs, %{actor_id: actor.id})
- case Conversations.create_comment(comment_data) do
+ case Discussions.create_comment(comment_data) do
{:ok, %Comment{} = comment} ->
assert comment.text == "some text"
assert comment.actor_id == actor.id
@@ -41,13 +39,13 @@ defmodule Mobilizon.ConversationsTest do
end
test "create_comment/1 with invalid data returns error changeset" do
- assert {:error, %Ecto.Changeset{}} = Conversations.create_comment(@invalid_attrs)
+ assert {:error, %Ecto.Changeset{}} = Discussions.create_comment(@invalid_attrs)
end
test "update_comment/2 with valid data updates the comment" do
%Comment{} = comment = insert(:comment)
- case Conversations.update_comment(comment, @update_attrs) do
+ case Discussions.update_comment(comment, @update_attrs) do
{:ok, %Comment{} = comment} ->
assert comment.text == "some updated text"
@@ -58,15 +56,15 @@ defmodule Mobilizon.ConversationsTest do
test "update_comment/2 with invalid data returns error changeset" do
%Comment{} = comment = insert(:comment)
- assert {:error, %Ecto.Changeset{}} = Conversations.update_comment(comment, @invalid_attrs)
- %Comment{} = comment_fetched = Conversations.get_comment!(comment.id)
+ assert {:error, %Ecto.Changeset{}} = Discussions.update_comment(comment, @invalid_attrs)
+ %Comment{} = comment_fetched = Discussions.get_comment!(comment.id)
assert comment = comment_fetched
end
test "delete_comment/1 deletes the comment" do
%Comment{} = comment = insert(:comment)
- assert {:ok, %Comment{}} = Conversations.delete_comment(comment)
- refute is_nil(Conversations.get_comment!(comment.id).deleted_at)
+ assert {:ok, %Comment{}} = Discussions.delete_comment(comment)
+ refute is_nil(Discussions.get_comment!(comment.id).deleted_at)
end
end
end
diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs
index 11bde5ed..71f74804 100644
--- a/test/mobilizon/events/events_test.exs
+++ b/test/mobilizon/events/events_test.exs
@@ -141,7 +141,7 @@ defmodule Mobilizon.EventsTest do
end
test "list_public_events_for_actor/1", %{actor: actor, event: event} do
- assert {:ok, [event_found], 1} = Events.list_public_events_for_actor(actor)
+ assert %Page{elements: [event_found], total: 1} = Events.list_public_events_for_actor(actor)
assert event_found.title == event.title
end
@@ -149,7 +149,7 @@ defmodule Mobilizon.EventsTest do
event1 = insert(:event, organizer_actor: actor)
case Events.list_public_events_for_actor(actor, 1, 10) do
- {:ok, events_found, 2} ->
+ %Page{elements: events_found, total: 2} ->
event_ids = MapSet.new(events_found |> Enum.map(& &1.id))
assert event_ids == MapSet.new([event.id, event1.id])
@@ -162,7 +162,7 @@ defmodule Mobilizon.EventsTest do
event1 = insert(:event, organizer_actor: actor)
case Events.list_public_events_for_actor(actor, 1, 1) do
- {:ok, [%Event{id: event_found_id}], 2} ->
+ %Page{elements: [%Event{id: event_found_id}], total: 2} ->
assert event_found_id in [event.id, event1.id]
err ->
diff --git a/test/mobilizon/posts_test.exs b/test/mobilizon/posts_test.exs
new file mode 100644
index 00000000..dd987674
--- /dev/null
+++ b/test/mobilizon/posts_test.exs
@@ -0,0 +1,82 @@
+defmodule Mobilizon.PostsTest do
+ use Mobilizon.DataCase
+
+ import Mobilizon.Factory
+
+ alias Mobilizon.Actors.Actor
+ alias Mobilizon.Posts
+ alias Mobilizon.Posts.Post
+
+ describe "posts" do
+ @valid_attrs %{body: "some text", title: "some title"}
+ @update_attrs %{body: "some updated text", title: "some updated title"}
+ @invalid_attrs %{body: nil}
+
+ test "list_posts/0 returns all posts" do
+ group = insert(:group)
+ %Post{id: post_id} = insert(:post, attributed_to: group)
+ post_ids = Posts.get_posts_for_group(group).elements |> Enum.map(& &1.id)
+ assert post_ids == [post_id]
+ end
+
+ test "get_post!/1 returns the post with given id" do
+ %Post{id: post_id} = insert(:post)
+ post_fetched = Posts.get_post(post_id)
+ assert post_fetched.id == post_id
+ end
+
+ test "create_post/1 with valid data creates a post" do
+ %Actor{} = actor = insert(:actor)
+ %Actor{} = group = insert(:group)
+ post_data = Map.merge(@valid_attrs, %{author_id: actor.id, attributed_to_id: group.id})
+
+ case Posts.create_post(post_data) do
+ {:ok, %Post{} = post} ->
+ assert post.body == "some text"
+ assert post.author_id == actor.id
+ assert post.title == "some title"
+ assert post.slug == "some-title-" <> ShortUUID.encode!(post.id)
+
+ err ->
+ flunk("Failed to create a post #{inspect(err)}")
+ end
+ end
+
+ test "create_post/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Posts.create_post(@invalid_attrs)
+ end
+
+ test "update_post/2 with valid data updates the post" do
+ %Actor{} = actor = insert(:actor)
+ %Actor{} = group = insert(:group)
+ post_data = Map.merge(@valid_attrs, %{author_id: actor.id, attributed_to_id: group.id})
+
+ {:ok, %Post{} = post} = Posts.create_post(post_data)
+
+ case Posts.update_post(post, @update_attrs) do
+ {:ok, %Post{} = updated_post} ->
+ assert updated_post.body == @update_attrs.body
+ assert updated_post.title == @update_attrs.title
+ # Slug and URL don't change
+ assert updated_post.slug == post.slug
+ assert updated_post.url == post.url
+
+ err ->
+ flunk("Failed to update a post #{inspect(err)}")
+ end
+ end
+
+ test "update_post/2 with invalid data returns error changeset" do
+ %Post{} = post = insert(:post)
+ assert {:error, %Ecto.Changeset{}} = Posts.update_post(post, @invalid_attrs)
+ %Post{} = post_fetched = Posts.get_post(post.id)
+ assert post = post_fetched
+ end
+
+ test "delete_post/1 deletes the post" do
+ %Post{} = post = insert(:post)
+ assert {:ok, %Post{}} = Posts.delete_post(post)
+ assert is_nil(Posts.get_post(post.id))
+ end
+ end
+end
diff --git a/test/service/geospatial/addok_test.exs b/test/service/geospatial/addok_test.exs
index 265cc371..abcd450d 100644
--- a/test/service/geospatial/addok_test.exs
+++ b/test/service/geospatial/addok_test.exs
@@ -1,91 +1,63 @@
defmodule Mobilizon.Service.Geospatial.AddokTest do
- use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
-
use Mobilizon.DataCase
- import Mock
+ import Mox
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Addok
-
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
- setup do
- # Config.instance_user_agent/0 makes database calls so because of ownership connection
- # we need to define it like this instead of a constant
- # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html
- {:ok,
- httpoison_headers: [
- {"User-Agent", Config.instance_user_agent()}
- ]}
- end
-
- @endpoint get_in(Application.get_env(:mobilizon, Addok), [:endpoint])
- @fake_endpoint "https://domain.tld"
+ alias Mobilizon.Service.HTTP.BaseClient.Mock
describe "search address" do
- test "produces a valid search address", %{httpoison_headers: httpoison_headers} do
- with_mock HTTPoison, get: fn _url, _headers, _options -> "{}" end do
- Addok.search("10 Rue Jangot")
-
- assert_called(
- HTTPoison.get(
- "#{@endpoint}/search/?q=10%20Rue%20Jangot&limit=10",
- httpoison_headers,
- @http_options
- )
- )
- end
- end
-
- test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do
- with_mock HTTPoison, get: fn _url, _headers, _options -> "{}" end do
- Addok.search("10 Rue Jangot",
- endpoint: @fake_endpoint,
- limit: 5,
- coords: %{lat: 49, lon: 12}
- )
-
- assert_called(
- HTTPoison.get(
- "#{@fake_endpoint}/search/?q=10%20Rue%20Jangot&limit=5&lat=49&lon=12",
- httpoison_headers,
- @http_options
- )
- )
- end
- end
-
test "returns a valid address from search" do
- use_cassette "geospatial/addok/search" do
- assert %Address{
- country: "France",
- region: "69, Rhône, Auvergne-Rhône-Alpes",
- locality: "Lyon",
- description: "10 Rue Jangot",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
- } == Addok.search("10 rue Jangot") |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/addok/search.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url: "https://api-adresse.data.gouv.fr/search/?q=10%20rue%20Jangot&limit=10"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert %Address{
+ country: "France",
+ region: "69, Rhône, Auvergne-Rhône-Alpes",
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
+ } == Addok.search("10 rue Jangot") |> hd
end
test "returns a valid address from reverse geocode" do
- use_cassette "geospatial/addok/geocode" do
- assert %Address{
- country: "France",
- region: "69, Rhône, Auvergne-Rhône-Alpes",
- locality: "Lyon",
- description: "10 Rue Jangot",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
- } == Addok.geocode(4.842569, 45.751718) |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/addok/geocode.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url: "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718&limit=10"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert %Address{
+ country: "France",
+ region: "69, Rhône, Auvergne-Rhône-Alpes",
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
+ } == Addok.geocode(4.842569, 45.751718) |> hd
end
end
end
diff --git a/test/service/geospatial/google_maps_test.exs b/test/service/geospatial/google_maps_test.exs
index 70a3e7c6..624fafbb 100644
--- a/test/service/geospatial/google_maps_test.exs
+++ b/test/service/geospatial/google_maps_test.exs
@@ -1,17 +1,11 @@
defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do
- use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
-
use Mobilizon.DataCase
- import Mock
+ import Mox
alias Mobilizon.Addresses.Address
alias Mobilizon.Service.Geospatial.GoogleMaps
-
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
+ alias Mobilizon.Service.HTTP.BaseClient.Mock
describe "search address" do
test "without API Key triggers an error" do
@@ -20,76 +14,117 @@ defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do
end
end
- test "produces a valid search address with options" do
- with_mock HTTPoison,
- get: fn _url, _headers, _options ->
- {:ok,
- %HTTPoison.Response{status_code: 200, body: "{\"status\": \"OK\", \"results\": []}"}}
- end do
- GoogleMaps.search("10 Rue Jangot",
- limit: 5,
- lang: "fr",
- api_key: "toto"
- )
-
- assert_called(
- HTTPoison.get(
- "https://maps.googleapis.com/maps/api/geocode/json?limit=5&key=toto&language=fr&address=10%20Rue%20Jangot",
- [],
- @http_options
- )
- )
- end
- end
-
test "triggers an error with an invalid API Key" do
+ data =
+ File.read!("test/fixtures/geospatial/google_maps/api_key_invalid.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=secret_key&language=en&address=10%20rue%20Jangot"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
assert_raise ArgumentError, "The provided API key is invalid.", fn ->
GoogleMaps.search("10 rue Jangot", api_key: "secret_key")
end
end
test "returns a valid address from search" do
- use_cassette "geospatial/google_maps/search" do
- assert %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "France",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.8424032, 45.75164940000001},
- properties: %{},
- srid: 4326
- },
- origin_id: "gm:ChIJtW0QikTq9EcRLI4Vy6bRx0U"
- } ==
- GoogleMaps.search("10 rue Jangot",
- api_key: "toto"
- )
- |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/google_maps/search.json")
+ |> Jason.decode!()
+
+ data_2 =
+ File.read!("test/fixtures/geospatial/google_maps/search_2.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, 3, fn
+ %{
+ method: :get,
+ url:
+ "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&address=10%20rue%20Jangot"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+
+ %{
+ method: :get,
+ url: _url
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data_2}}
+ end)
+
+ assert %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "France",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.8424032, 45.75164940000001},
+ properties: %{},
+ srid: 4326
+ },
+ origin_id: "gm:ChIJtW0QikTq9EcRLI4Vy6bRx0U"
+ } ==
+ GoogleMaps.search("10 rue Jangot",
+ api_key: "toto"
+ )
+ |> hd
end
test "returns a valid address from reverse geocode" do
- use_cassette "geospatial/google_maps/geocode" do
- assert %Address{
- locality: "Lyon",
- description: "10bis Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "France",
- postal_code: "69007",
- street: "10bis Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.8424966, 45.751725},
- properties: %{},
- srid: 4326
- },
- origin_id: "gm:ChIJrW0QikTq9EcR96jk2OnO75w"
- } ==
- GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto")
- |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/google_maps/geocode.json")
+ |> Jason.decode!()
+
+ data_2 =
+ File.read!("test/fixtures/geospatial/google_maps/geocode_2.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, 3, fn
+ %{
+ method: :get,
+ url:
+ "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&latlng=45.751718,4.842569&result_type=street_address"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+
+ %{
+ method: :get,
+ url: _url
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data_2}}
+ end)
+
+ assert %Address{
+ locality: "Lyon",
+ description: "10bis Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "France",
+ postal_code: "69007",
+ street: "10bis Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.8424966, 45.751725},
+ properties: %{},
+ srid: 4326
+ },
+ origin_id: "gm:ChIJrW0QikTq9EcR96jk2OnO75w"
+ } ==
+ GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto")
+ |> hd
end
end
end
diff --git a/test/service/geospatial/map_quest_test.exs b/test/service/geospatial/map_quest_test.exs
index ff25e815..937fd652 100644
--- a/test/service/geospatial/map_quest_test.exs
+++ b/test/service/geospatial/map_quest_test.exs
@@ -1,28 +1,11 @@
defmodule Mobilizon.Service.Geospatial.MapQuestTest do
- use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
-
use Mobilizon.DataCase
- import Mock
+ import Mox
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.MapQuest
-
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
- setup do
- # Config.instance_user_agent/0 makes database calls so because of ownership connection
- # we need to define it like this instead of a constant
- # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html
- {:ok,
- httpoison_headers: [
- {"User-Agent", Config.instance_user_agent()}
- ]}
- end
+ alias Mobilizon.Service.HTTP.BaseClient.Mock
describe "search address" do
test "without API Key triggers an error" do
@@ -31,75 +14,88 @@ defmodule Mobilizon.Service.Geospatial.MapQuestTest do
end
end
- test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do
- with_mock HTTPoison,
- get: fn _url, _headers, _options ->
- {:ok,
- %HTTPoison.Response{
- status_code: 200,
- body: "{\"info\": {\"statuscode\": 0}, \"results\": []}"
- }}
- end do
- MapQuest.search("10 Rue Jangot",
- limit: 5,
- lang: "fr",
- api_key: "toto"
- )
-
- assert_called(
- HTTPoison.get(
- "https://open.mapquestapi.com/geocoding/v1/address?key=toto&location=10%20Rue%20Jangot&maxResults=5",
- httpoison_headers,
- @http_options
- )
- )
- end
- end
-
test "triggers an error with an invalid API Key" do
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10"
+ },
+ _opts ->
+ {:ok,
+ %Tesla.Env{status: 403, body: "The AppKey submitted with this request is invalid."}}
+ end)
+
assert_raise ArgumentError, "The AppKey submitted with this request is invalid.", fn ->
MapQuest.search("10 rue Jangot", api_key: "secret_key")
end
end
test "returns a valid address from search" do
- use_cassette "geospatial/map_quest/search" do
- assert %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "FR",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.842566, 45.751714},
- properties: %{},
- srid: 4326
- }
- } ==
- MapQuest.search("10 rue Jangot", api_key: "secret_key")
- |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/map_quest/search.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "FR",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.842566, 45.751714},
+ properties: %{},
+ srid: 4326
+ }
+ } ==
+ MapQuest.search("10 rue Jangot", api_key: "secret_key")
+ |> hd
end
test "returns a valid address from reverse geocode" do
- use_cassette "geospatial/map_quest/geocode" do
- assert %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "FR",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.842569, 45.751718},
- properties: %{},
- srid: 4326
- }
- } ==
- MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key")
- |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/map_quest/geocode.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "FR",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.842569, 45.751718},
+ properties: %{},
+ srid: 4326
+ }
+ } ==
+ MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key")
+ |> hd
end
end
end
diff --git a/test/service/geospatial/nominatim_test.exs b/test/service/geospatial/nominatim_test.exs
index cd01f760..f3dedf65 100644
--- a/test/service/geospatial/nominatim_test.exs
+++ b/test/service/geospatial/nominatim_test.exs
@@ -1,91 +1,81 @@
defmodule Mobilizon.Service.Geospatial.NominatimTest do
- use Mobilizon.DataCase, async: false
- use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
- import Mock
+ use Mobilizon.DataCase
+ import Mox
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Nominatim
-
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
- setup do
- # Config.instance_user_agent/0 makes database calls so because of ownership connection
- # we need to define it like this instead of a constant
- # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html
- {:ok,
- httpoison_headers: [
- {"User-Agent", Config.instance_user_agent()}
- ]}
- end
+ alias Mobilizon.Service.HTTP.BaseClient.Mock
describe "search address" do
- test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do
- with_mock HTTPoison,
- get: fn _url, _headers, _options ->
- {:ok, %HTTPoison.Response{status_code: 200, body: "[]"}}
- end do
- Nominatim.search("10 Rue Jangot",
- limit: 5,
- lang: "fr"
- )
-
- assert_called(
- HTTPoison.get(
- "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1&namedetails=1",
- httpoison_headers,
- @http_options
- )
- )
- end
- end
-
test "returns a valid address from search" do
- use_cassette "geospatial/nominatim/search" do
- assert [
- %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "France",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.8425657, 45.7517141},
- properties: %{},
- srid: 4326
- },
- origin_id: "nominatim:3078260611",
- type: "house"
- }
- ] == Nominatim.search("10 rue Jangot")
- end
+ data =
+ File.read!("test/fixtures/geospatial/nominatim/search.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=1"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert [
+ %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "France",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.8425657, 45.7517141},
+ properties: %{},
+ srid: 4326
+ },
+ origin_id: "nominatim:3078260611",
+ type: "house"
+ }
+ ] == Nominatim.search("10 rue Jangot")
end
test "returns a valid address from reverse geocode" do
- use_cassette "geospatial/nominatim/geocode" do
- assert [
- %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "France",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.8425657, 45.7517141},
- properties: %{},
- srid: 4326
- },
- origin_id: "nominatim:3078260611",
- type: "house"
- }
- ] ==
- Nominatim.geocode(4.842569, 45.751718)
- end
+ data =
+ File.read!("test/fixtures/geospatial/nominatim/geocode.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url:
+ "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=1"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert [
+ %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "France",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.8425657, 45.7517141},
+ properties: %{},
+ srid: 4326
+ },
+ origin_id: "nominatim:3078260611",
+ type: "house"
+ }
+ ] ==
+ Nominatim.geocode(4.842569, 45.751718)
end
end
end
diff --git a/test/service/geospatial/photon_test.exs b/test/service/geospatial/photon_test.exs
index 3d8d2185..c5fa9fb9 100644
--- a/test/service/geospatial/photon_test.exs
+++ b/test/service/geospatial/photon_test.exs
@@ -1,66 +1,41 @@
defmodule Mobilizon.Service.Geospatial.PhotonTest do
- use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
-
use Mobilizon.DataCase
- import Mock
+ import Mox
alias Mobilizon.Addresses.Address
- alias Mobilizon.Config
alias Mobilizon.Service.Geospatial.Photon
-
- @http_options [
- follow_redirect: true,
- ssl: [{:versions, [:"tlsv1.2"]}]
- ]
-
- setup do
- # Config.instance_user_agent/0 makes database calls so because of ownership connection
- # we need to define it like this instead of a constant
- # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html
- {:ok,
- httpoison_headers: [
- {"User-Agent", Config.instance_user_agent()}
- ]}
- end
+ alias Mobilizon.Service.HTTP.BaseClient.Mock
describe "search address" do
- test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do
- with_mock HTTPoison,
- get: fn _url, _headers, _options ->
- {:ok, %HTTPoison.Response{status_code: 200, body: "{\"features\": []"}}
- end do
- Photon.search("10 Rue Jangot",
- limit: 5,
- lang: "fr"
- )
-
- assert_called(
- HTTPoison.get(
- "https://photon.komoot.de/api/?q=10%20Rue%20Jangot&lang=fr&limit=5",
- httpoison_headers,
- @http_options
- )
- )
- end
- end
-
test "returns a valid address from search" do
- use_cassette "geospatial/photon/search" do
- assert %Address{
- locality: "Lyon",
- description: "10 Rue Jangot",
- region: "Auvergne-Rhône-Alpes",
- country: "France",
- postal_code: "69007",
- street: "10 Rue Jangot",
- geom: %Geo.Point{
- coordinates: {4.8425657, 45.7517141},
- properties: %{},
- srid: 4326
- }
- } == Photon.search("10 rue Jangot") |> hd
- end
+ data =
+ File.read!("test/fixtures/geospatial/photon/search.json")
+ |> Jason.decode!()
+
+ Mock
+ |> expect(:call, fn
+ %{
+ method: :get,
+ url: "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10"
+ },
+ _opts ->
+ {:ok, %Tesla.Env{status: 200, body: data}}
+ end)
+
+ assert %Address{
+ locality: "Lyon",
+ description: "10 Rue Jangot",
+ region: "Auvergne-Rhône-Alpes",
+ country: "France",
+ postal_code: "69007",
+ street: "10 Rue Jangot",
+ geom: %Geo.Point{
+ coordinates: {4.8425657, 45.7517141},
+ properties: %{},
+ srid: 4326
+ }
+ } == Photon.search("10 rue Jangot") |> hd
end
# Photon returns something quite wrong, so no need to test this right now.
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 61b22b62..79b30165 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -73,4 +73,7 @@ defmodule Mobilizon.DataCase do
:ok
end
+
+ Mox.defmock(Mobilizon.Service.HTTP.ActivityPub.Mock, for: Tesla.Adapter)
+ Mox.defmock(Mobilizon.Service.HTTP.BaseClient.Mock, for: Tesla.Adapter)
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 7a9784fe..84cf06dc 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -118,12 +118,15 @@ defmodule Mobilizon.Factory do
def comment_factory do
uuid = Ecto.UUID.generate()
- %Mobilizon.Conversations.Comment{
+ %Mobilizon.Discussions.Comment{
text: "My Comment",
actor: build(:actor),
event: build(:event),
uuid: uuid,
mentions: [],
+ attributed_to: nil,
+ local: true,
+ deleted_at: nil,
tags: build_list(3, :tag),
in_reply_to_comment: nil,
url: Routes.page_url(Endpoint, :comment, uuid)
@@ -142,6 +145,7 @@ defmodule Mobilizon.Factory do
begins_on: start,
ends_on: Timex.shift(start, hours: 2),
organizer_actor: actor,
+ attributed_to: nil,
category: sequence("something"),
physical_address: build(:address),
visibility: :public,
@@ -324,4 +328,50 @@ defmodule Mobilizon.Factory do
value: sequence("value")
}
end
+
+ def post_factory do
+ uuid = Ecto.UUID.generate()
+
+ %Mobilizon.Posts.Post{
+ body: "The
HTMLbody for my Article",
+ title: "My Awesome article",
+ slug: "my-awesome-article-#{ShortUUID.encode!(uuid)}",
+ author: build(:actor),
+ attributed_to: build(:group),
+ id: uuid,
+ draft: false,
+ tags: build_list(3, :tag),
+ visibility: :public,
+ publish_at: DateTime.utc_now(),
+ url: Routes.page_url(Endpoint, :post, uuid)
+ }
+ end
+
+ def tombstone_factory do
+ uuid = Ecto.UUID.generate()
+
+ %Mobilizon.Tombstone{
+ uri: "https://mobilizon.test/comments/#{uuid}",
+ actor: build(:actor)
+ }
+ end
+
+ def discussion_factory do
+ uuid = Ecto.UUID.generate()
+ actor = build(:actor)
+ group = build(:group)
+ comment = build(:comment, actor: actor, attributed_to: group)
+ slug = "my-awesome-discussion-#{ShortUUID.encode!(uuid)}"
+
+ %Mobilizon.Discussions.Discussion{
+ title: "My Awesome discussion",
+ slug: slug,
+ creator: actor,
+ actor: group,
+ id: uuid,
+ last_comment: comment,
+ comments: [comment],
+ url: Routes.page_url(Endpoint, :discussion, group.preferred_username, slug)
+ }
+ end
end
diff --git a/test/web/controllers/activity_pub_controller_test.exs b/test/web/controllers/activity_pub_controller_test.exs
index 8777cbae..e5dcbf92 100644
--- a/test/web/controllers/activity_pub_controller_test.exs
+++ b/test/web/controllers/activity_pub_controller_test.exs
@@ -344,11 +344,12 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
Actors.create_group(%{
creator_actor_id: actor.id,
preferred_username: "my_group",
- visibility: :public
+ local: true
})
result =
conn
+ |> assign(:actor, actor)
|> get(Actor.build_url(group.preferred_username, :members))
|> json_response(200)
@@ -358,15 +359,21 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
assert hd(result["first"]["orderedItems"])["type"] == "Member"
end
- test "it returns no members for a private group", %{conn: conn} do
+ test "it returns no members when not a member of the group", %{conn: conn} do
actor = insert(:actor)
+ actor2 = insert(:actor)
assert {:ok, %Actor{} = group} =
- Actors.create_group(%{creator_actor_id: actor.id, preferred_username: "my_group"})
+ Actors.create_group(%{
+ creator_actor_id: actor.id,
+ preferred_username: "my_group",
+ local: true
+ })
result =
conn
- |> get(Actor.build_url(actor.preferred_username, :members))
+ |> assign(:actor, actor2)
+ |> get(Actor.build_url(group.preferred_username, :members))
|> json_response(200)
assert result["first"]["orderedItems"] == []
@@ -379,7 +386,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
Actors.create_group(%{
creator_actor_id: actor.id,
preferred_username: "my_group",
- visibility: :public
+ local: true
})
Enum.each(1..15, fn _ ->
@@ -389,6 +396,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
result =
conn
+ |> assign(:actor, actor)
|> get(Actor.build_url(group.preferred_username, :members))
|> json_response(200)
@@ -398,6 +406,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
result =
conn
+ |> assign(:actor, actor)
|> get(Actor.build_url(group.preferred_username, :members, page: 2))
|> json_response(200)
@@ -411,7 +420,8 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
assert {:ok, %Actor{} = group} =
Actors.create_group(%{
creator_actor_id: actor_group_admin.id,
- preferred_username: "my_group"
+ preferred_username: "my_group",
+ local: true
})
insert(:member, actor: actor_applicant, parent: group, role: :member)
diff --git a/test/web/controllers/feed_controller_test.exs b/test/web/controllers/feed_controller_test.exs
index e4247128..e6343a2e 100644
--- a/test/web/controllers/feed_controller_test.exs
+++ b/test/web/controllers/feed_controller_test.exs
@@ -91,24 +91,26 @@ defmodule Mobilizon.Web.FeedControllerTest do
describe "/@:preferred_username/feed/ics" do
test "it returns an iCalendar representation of the actor's public events with an actor publicly visible",
%{conn: conn} do
- actor = insert(:actor, visibility: :public)
+ actor = insert(:actor)
+ group = insert(:group, visibility: :public)
tag1 = insert(:tag, title: "iCalendar", slug: "icalendar")
tag2 = insert(:tag, title: "Apple", slug: "apple")
- event1 = insert(:event, organizer_actor: actor, tags: [tag1])
- event2 = insert(:event, organizer_actor: actor, tags: [tag1, tag2])
+ event1 = insert(:event, organizer_actor: actor, attributed_to: group, tags: [tag1])
+ event2 = insert(:event, organizer_actor: actor, attributed_to: group, tags: [tag1, tag2])
conn =
conn
|> get(
Endpoint
- |> Routes.feed_url(:actor, actor.preferred_username, "ics")
+ |> Routes.feed_url(:actor, group.preferred_username, "ics")
|> URI.decode()
)
- assert response(conn, 200) =~ "BEGIN:VCALENDAR"
+ assert res = response(conn, 200)
+ assert res =~ "BEGIN:VCALENDAR"
assert response_content_type(conn, :calendar) =~ "charset=utf-8"
- [entry1, entry2] = entries = ExIcal.parse(conn.resp_body)
+ [entry1, entry2] = entries = ExIcal.parse(res)
Enum.each(entries, fn entry ->
assert entry.summary in [event1.title, event2.title]
@@ -120,7 +122,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
test "it returns a 404 page for the actor's public events iCal feed with an actor not publicly visible",
%{conn: conn} do
- actor = insert(:actor, visibility: :private)
+ actor = insert(:group, visibility: :private)
tag1 = insert(:tag, title: "iCalendar", slug: "icalendar")
tag2 = insert(:tag, title: "Apple", slug: "apple")
insert(:event, organizer_actor: actor, tags: [tag1])