You requested a full backup of your Pleroma account. It's ready for download:
\nYour account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.
\n"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:202
+#, elixir-autogen, elixir-format
msgctxt "approval pending email subject"
msgid "Your account is awaiting approval"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:158
+#, elixir-autogen, elixir-format
msgctxt "confirmation email body"
msgid "Email confirmation is required to activate the account.
\nSomeone has requested password change for your account at %{instance_name}.
\nIf it was someone else, nothing to worry about: your data is secure and your password has not been changed.
\n"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:98
+#, elixir-autogen, elixir-format
msgctxt "password reset email subject"
msgid "Password reset"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:215
+#, elixir-autogen, elixir-format
msgctxt "successful registration email body"
msgid "Your account at %{instance_name} has been registered successfully.
\nNo further action is required to activate your account.
\n"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:231
+#, elixir-autogen, elixir-format
msgctxt "successful registration email subject"
msgid "Account registered on %{instance_name}"
msgstr ""
-#, elixir-autogen, elixir-format
#: lib/pleroma/emails/user_email.ex:119
+#, elixir-autogen, elixir-format
msgctxt "user invitation email body"
msgid "%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.
\nAdmin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:
\n
\ No newline at end of file
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 650118475..b499a96f5 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -17,6 +17,7 @@
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
+ "fedibird": "http://fedibird.com/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
@@ -26,6 +27,8 @@
"@id": "litepub:listMessage",
"@type": "@id"
},
+ "quoteUrl": "as:quoteUrl",
+ "quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
diff --git a/priv/static/static/config.json b/priv/static/static/config.json
index 53a4be823..fb39ff77f 100644
--- a/priv/static/static/config.json
+++ b/priv/static/static/config.json
@@ -14,6 +14,7 @@
"logoMask": true,
"logoLeft": false,
"minimalScopesMode": false,
+ "disableUpdateNotification": false,
"nsfwCensorImage": "",
"postContentType": "text/plain",
"redirectRootLogin": "/main/friends",
diff --git a/priv/static/static/css/159.1d523a00378ebd68c5b3.css b/priv/static/static/css/159.1d523a00378ebd68c5b3.css
new file mode 100644
index 000000000..146838cff
Binary files /dev/null and b/priv/static/static/css/159.1d523a00378ebd68c5b3.css differ
diff --git a/priv/static/static/css/159.1d523a00378ebd68c5b3.css.map b/priv/static/static/css/159.1d523a00378ebd68c5b3.css.map
new file mode 100644
index 000000000..cb7151a69
--- /dev/null
+++ b/priv/static/static/css/159.1d523a00378ebd68c5b3.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/159.1d523a00378ebd68c5b3.css","mappings":"AAGA,gBACE,WAEA,0BACE,iBAEA,kDACE,aACA,eACA,cAEA,2DACE,aACA,cAGA,YAFA,WACA,UACA,CAEA,+DACE,YAEA,qEACE","sources":["webpack://pleroma_fe/./src/components/sticker_picker/sticker_picker.vue"],"sourcesContent":["\n@import \"../../variables\";\n\n.sticker-picker {\n width: 100%;\n\n .contents {\n min-height: 250px;\n\n .sticker-picker-content {\n display: flex;\n flex-wrap: wrap;\n padding: 0 4px;\n\n .sticker {\n display: flex;\n flex: 1 1 auto;\n margin: 4px;\n width: 56px;\n height: 56px;\n\n img {\n height: 100%;\n\n &:hover {\n filter: drop-shadow(0 0 5px var(--accent, $fallback--link));\n }\n }\n }\n }\n }\n}\n\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css b/priv/static/static/css/2.0778a6a864a1307a6c41.css
deleted file mode 100644
index a33585ef1..000000000
Binary files a/priv/static/static/css/2.0778a6a864a1307a6c41.css and /dev/null differ
diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css.map b/priv/static/static/css/2.0778a6a864a1307a6c41.css.map
deleted file mode 100644
index 28cd8ba54..000000000
--- a/priv/static/static/css/2.0778a6a864a1307a6c41.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/2.0778a6a864a1307a6c41.css","sourcesContent":[".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/3.b2603a50868c68a1c192.css b/priv/static/static/css/3.b2603a50868c68a1c192.css
deleted file mode 100644
index 4cec5785b..000000000
Binary files a/priv/static/static/css/3.b2603a50868c68a1c192.css and /dev/null differ
diff --git a/priv/static/static/css/3.b2603a50868c68a1c192.css.map b/priv/static/static/css/3.b2603a50868c68a1c192.css.map
deleted file mode 100644
index 805e7dc04..000000000
--- a/priv/static/static/css/3.b2603a50868c68a1c192.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/3.b2603a50868c68a1c192.css","sourcesContent":["/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/6464.169260b661120cc50815.css b/priv/static/static/css/6464.169260b661120cc50815.css
new file mode 100644
index 000000000..240087a1d
Binary files /dev/null and b/priv/static/static/css/6464.169260b661120cc50815.css differ
diff --git a/priv/static/static/css/6464.169260b661120cc50815.css.map b/priv/static/static/css/6464.169260b661120cc50815.css.map
new file mode 100644
index 000000000..048efb2b9
--- /dev/null
+++ b/priv/static/static/css/6464.169260b661120cc50815.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/6464.169260b661120cc50815.css","mappings":"AAEA,oBACE,gBAGF,yBACE,mBAAoB,CACpB,sBAAuB,CACvB,oBAAqB,CAErB,eACA,kBACA,qBAEA,wBADA,sCACA,CAEA,+BACE,eACA,iBAGF,yBAhBF,yBAqBI,aAGF,0BAxBF,yBAyBI,cAGF,kCAGE,8CACA,4CAFA,wCADA,eAGA,CAGE,iDACE,oCAKN,qCAGE,gCADA,mBADA,oBAEA,CAGF,uCAGE,eACA,2BAFA,kBADA,UAGA,CAGF,sCAWE,gDAJA,YANA,qCACA,2CAUA,oBAHA,kBACA,kBAPA,+DAEA,wBADA,uCAEA,WAEA,UAIA,CAGF,qCACE,+BAGF,wCACE,kCAGF,2CAKE,6GACE,CADF,sGADA,gBAHA,qCAEA,wBADA,kCAIE,CAIJ,qCACE,iBAGF,+BAKE,uCAEA,4CACE,YAEA,0BADA,UACA,CAGF,iDACE","sources":["webpack://pleroma_fe/./src/components/update_notification/update_notification.scss"],"sourcesContent":["@import \"src/variables\";\n\n.UpdateNotification {\n overflow: hidden;\n}\n\n.UpdateNotificationModal {\n --__top-fringe: 15em; // how much pleroma-tan should stick her head above\n --__bottom-fringe: 80em; // just reserving as much as we can, number is mostly irrelevant\n --__right-fringe: 8em;\n\n font-size: 15px;\n position: relative;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 500ms;\n\n .text {\n max-width: 40em;\n padding-left: 1em;\n }\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser\n bottom bar regardless of whether or not it is visible.\n */\n width: 100vw;\n }\n\n @media all and (max-height: 600px) {\n display: none;\n }\n\n .content {\n overflow: hidden;\n margin-top: calc(-1 * var(--__top-fringe));\n margin-bottom: calc(-1 * var(--__bottom-fringe));\n margin-right: calc(-1 * var(--__right-fringe));\n\n &.-noImage {\n .text {\n padding-right: var(--__right-fringe);\n }\n }\n }\n\n .panel-body {\n border-width: 0 0 1px;\n border-style: solid;\n border-color: var(--border, $fallback--border);\n }\n\n .panel-footer {\n z-index: 22;\n position: relative;\n border-width: 0;\n grid-template-columns: auto;\n }\n\n .pleroma-tan {\n object-fit: cover;\n object-position: top;\n transition: position, left, right, top, bottom, max-width, max-height;\n transition-timing-function: ease-in-out;\n transition-duration: 500ms;\n width: 25em;\n float: right;\n z-index: 20;\n position: relative;\n shape-margin: 0.5em;\n filter: drop-shadow(5px 5px 10px rgb(0 0 0 / 50%));\n pointer-events: none;\n }\n\n .spacer-top {\n min-height: var(--__top-fringe);\n }\n\n .spacer-bottom {\n min-height: var(--__bottom-fringe);\n }\n\n .extra-info-group {\n transition: max-height, padding, height;\n transition-timing-function: ease-in;\n transition-duration: 700ms;\n max-height: 70vh;\n mask:\n linear-gradient(to top, white, transparent) bottom/100% 2px no-repeat,\n linear-gradient(to top, white, white);\n }\n\n .art-credit {\n text-align: right;\n }\n\n &.-peek {\n /* Explanation:\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n */\n transform: translateY(calc(((100vh - 100%) / 2)));\n\n .pleroma-tan {\n float: right;\n z-index: 10;\n shape-image-threshold: 70%;\n }\n\n .extra-info-group {\n max-height: 0;\n }\n }\n}\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css b/priv/static/static/css/7586.0d43f70bc6240422f179.css
new file mode 100644
index 000000000..7da2aa2ea
Binary files /dev/null and b/priv/static/static/css/7586.0d43f70bc6240422f179.css differ
diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css.map b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map
new file mode 100644
index 000000000..f8f61fe6e
--- /dev/null
+++ b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/7586.0d43f70bc6240422f179.css","mappings":"AACA,uBAGE,mBAFA,aACA,YAEA,uBAEA,4BACE,YACA,iBCPJ,gBACE,gBAEA,2DAEE,qBACA,iBAEA,iEACE,mBAGF,mFACE,gBAIJ,qCAGE,cADA,kBADA,eAEA,CAGF,sCAOE,YADA,eALA,gBACA,qBAEA,wBADA,uCAEA,YAEA,CAEA,yBATF,sCAWI,YADA,eACA,EAGF,kDACE,YACA,kBAEA,uDACE,eAGF,6EACE,cAKN,iCACE,aACA,eACA,cAEA,mCACE,kBAGF,gDACE,aACA,YAKF,2CASE,8CAEA,yBAXF,2CAgBI","sources":["webpack://pleroma_fe/./src/components/async_component_error/async_component_error.vue","webpack://pleroma_fe/./src/components/settings_modal/settings_modal.scss"],"sourcesContent":["\n.async-component-error {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n\n .btn {\n margin: 0.5em;\n padding: 0.5em 2em;\n }\n}\n","@import \"src/variables\";\n\n.settings-modal {\n overflow: hidden;\n\n .setting-list,\n .option-list {\n list-style-type: none;\n padding-left: 2em;\n\n li {\n margin-bottom: 0.5em;\n }\n\n .suboptions {\n margin-top: 0.3em;\n }\n }\n\n .setting-description {\n margin-top: 0.2em;\n margin-bottom: 2em;\n font-size: 70%;\n }\n\n .settings-modal-panel {\n overflow: hidden;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 300ms;\n width: 1000px;\n max-width: 90vw;\n height: 90vh;\n\n @media all and (max-width: 800px) {\n max-width: 100vw;\n height: 100%;\n }\n\n >.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n }\n\n .btn:not(.dropdown-button) {\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n flex-wrap: wrap;\n line-height: 2;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css
new file mode 100644
index 000000000..2326ed932
Binary files /dev/null and b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css differ
diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map
new file mode 100644
index 000000000..9d501f27a
--- /dev/null
+++ b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/7962.76663e78ad5ea0bb0b90.css","mappings":"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","sources":["webpack://pleroma_fe/./src/components/importer/importer.vue","webpack://pleroma_fe/./src/components/exporter/exporter.vue","webpack://pleroma_fe/./src/components/autosuggest/autosuggest.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/block_card/block_card.vue","webpack://pleroma_fe/./src/components/mute_card/mute_card.vue","webpack://pleroma_fe/./src/components/domain_mute_card/domain_mute_card.vue","webpack://pleroma_fe/./src/components/selectable_list/selectable_list.vue","webpack://pleroma_fe/./src/hocs/with_subscription/with_subscription.scss","webpack://pleroma_fe/./src/components/settings_modal/tabs/mutes_and_blocks_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/modified_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/profile_setting_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/draft_buttons.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa.vue","webpack://pleroma_fe/./node_modules/cropperjs/dist/cropper.css","webpack://pleroma_fe/./src/components/image_cropper/image_cropper.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/profile_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/size_setting.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/general_tab.vue","webpack://pleroma_fe/./src/components/color_input/color_input.scss","webpack://pleroma_fe/./src/components/color_input/color_input.vue","webpack://pleroma_fe/./src/components/shadow_control/shadow_control.vue","webpack://pleroma_fe/./src/components/font_control/font_control.vue","webpack://pleroma_fe/./src/components/contrast_ratio/contrast_ratio.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/preview.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/theme_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/settings_modal_user_content.scss"],"sourcesContent":["\n.importer {\n &-uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n}\n","\n.exporter {\n &-processing {\n margin: 0.25em;\n }\n}\n","\n@import \"../../variables\";\n\n.autosuggest {\n position: relative;\n\n &-input {\n display: block;\n width: 100%;\n }\n\n &-results {\n position: absolute;\n left: 0;\n top: 100%;\n right: 0;\n max-height: 400px;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-style: solid;\n border-width: 1px;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n overflow-y: auto;\n z-index: 1;\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n","\n.block-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.mute-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.domain-mute-card {\n flex: 1 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0.6em 1em 0.6em 0;\n\n &-domain {\n margin-right: 1em;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n button {\n width: 10em;\n }\n\n .autosuggest-results & {\n padding-left: 1em;\n }\n}\n","\n@import \"../../variables\";\n\n.selectable-list {\n &-item-inner {\n display: flex;\n align-items: center;\n\n > * {\n min-width: 0;\n }\n }\n\n &-item-selected-inner {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &-header {\n display: flex;\n align-items: center;\n padding: 0.6em 0;\n border-bottom: 2px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n\n &-actions {\n flex: 1;\n }\n }\n\n &-checkbox-wrapper {\n padding: 0 10px;\n flex: none;\n }\n}\n",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 1rem;\n }\n }\n}\n",".mutes-and-blocks-tab {\n height: 100%;\n\n .usersearch-wrapper {\n padding: 1em;\n }\n\n .bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n }\n\n .bulk-action-button {\n width: 10em;\n }\n\n .domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n }\n\n .domain-mute-button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n}\n","\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ProfileSettingIndicator {\n display: inline-block;\n position: relative;\n}\n\n.profilesetting-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.DraftButtons {\n display: inline-block;\n position: relative;\n\n .button-default {\n margin-left: 0.5em;\n }\n}\n\n.draft-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n@import \"../../../../variables\";\n\n.mfa-backup-codes {\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .backup-codes {\n font-family: var(--postCodeFont, monospace);\n }\n}\n","\n@import \"../../../../variables\";\n\n.mfa-settings {\n .mfa-heading,\n .method-item {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n }\n\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .setup-otp {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n\n .qr-code {\n flex: 1;\n padding-right: 10px;\n }\n .verify { flex: 1; }\n .error { margin: 4px 0 0; }\n\n .confirm-otp-actions {\n button {\n width: 15em;\n margin-top: 5px;\n }\n }\n }\n}\n","/*!\n * Cropper.js v1.5.13\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2022-11-20T05:30:43.444Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n }\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 75%);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n }\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n }\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n .cropper-center::after {\n background-color: #eee;\n content: \" \";\n display: block;\n position: absolute;\n }\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n }\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n }\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n }\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n }\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n }\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n }\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n }\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n }\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n }\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n }\n\n@media (min-width: 768px) {\n\n.cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n }\n\n@media (min-width: 992px) {\n\n.cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n }\n\n@media (min-width: 1200px) {\n\n.cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n }\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: \" \";\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n }\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC\");\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n","\n.image-cropper {\n &-img-input {\n display: none;\n }\n\n &-image-container {\n position: relative;\n\n img {\n display: block;\n max-width: 100%;\n }\n }\n\n &-buttons-wrapper {\n margin-top: 10px;\n\n button {\n margin-top: 5px;\n }\n }\n}\n","@import \"../../../variables\";\n\n.profile-tab {\n .bio {\n margin: 0;\n }\n\n .visibility-tray {\n padding-top: 5px;\n }\n\n input[type=\"file\"] {\n padding: 5px;\n height: auto;\n }\n\n .banner-background-preview {\n max-width: 100%;\n width: 300px;\n position: relative;\n\n img {\n width: 100%;\n }\n }\n\n .uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n\n .name-changer {\n width: 100%;\n }\n\n .current-avatar-container {\n position: relative;\n width: 150px;\n height: 150px;\n }\n\n .current-avatar {\n display: block;\n width: 100%;\n height: 100%;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n }\n\n .reset-button {\n position: absolute;\n top: 0.2em;\n right: 0.2em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n background-color: rgb(0 0 0 / 60%);\n opacity: 0.7;\n width: 1.5em;\n height: 1.5em;\n text-align: center;\n line-height: 1.5em;\n font-size: 1.5em;\n cursor: pointer;\n\n &:hover {\n opacity: 1;\n }\n\n svg {\n color: white;\n }\n }\n\n .oauth-tokens {\n width: 100%;\n\n th {\n text-align: left;\n }\n\n .actions {\n text-align: right;\n }\n }\n\n &-usersearch-wrapper {\n padding: 1em;\n }\n\n &-bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n\n button {\n width: 10em;\n }\n }\n\n &-domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n\n button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n }\n\n .setting-subitem {\n margin-left: 1.75em;\n }\n\n .profile-fields {\n display: flex;\n\n & > .emoji-input {\n flex: 1 1 auto;\n margin: 0 0.2em 0.5em;\n min-width: 0;\n }\n\n .delete-field {\n width: 20px;\n align-self: center;\n margin: 0 0.2em 0.5em;\n padding: 0 0.5em;\n }\n }\n\n .birthday-input {\n display: block;\n margin-bottom: 1em;\n }\n}\n","\n.SizeSetting {\n .number-input {\n max-width: 6.5em;\n }\n\n .css-unit-input,\n .css-unit-input select {\n margin-left: 0.5em;\n width: 4em;\n max-width: 4em;\n min-width: 4em;\n }\n}\n\n","\n.column-settings {\n display: flex;\n justify-content: space-evenly;\n flex-wrap: wrap;\n}\n\n.column-settings .size-label {\n display: block;\n margin-bottom: 0.5em;\n margin-top: 0.5em;\n}\n","@import \"../../variables\";\n\n.color-input {\n display: inline-flex;\n\n &-field.input {\n display: inline-flex;\n flex: 0 0 0;\n max-width: 9em;\n align-items: stretch;\n padding: 0.2em 8px;\n\n input {\n background: none;\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n border: none;\n padding: 0;\n margin: 0;\n\n &.textColor {\n flex: 1 0 3em;\n min-width: 3em;\n padding: 0;\n }\n\n &.nativeColor {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n }\n\n .computedIndicator,\n .transparentIndicator {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n\n .transparentIndicator {\n // forgot to install counter-strike source, ooops\n background-color: #f0f;\n position: relative;\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n background-color: #000;\n position: absolute;\n height: 50%;\n width: 50%;\n }\n\n &::after {\n top: 0;\n left: 0;\n }\n\n &::before {\n bottom: 0;\n right: 0;\n }\n }\n }\n\n .label {\n flex: 1 1 auto;\n }\n}\n","\n.color-control {\n input.text-input {\n max-width: 7em;\n flex: 1;\n }\n}\n","\n@import \"../../variables\";\n\n.shadow-control {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n margin-bottom: 1em;\n\n .shadow-preview-container,\n .shadow-tweak {\n margin: 5px 6px 0 0;\n }\n\n .shadow-preview-container {\n flex: 0;\n display: flex;\n flex-wrap: wrap;\n\n $side: 15em;\n\n input[type=\"number\"] {\n width: 5em;\n min-width: 2em;\n }\n\n .x-shift-control,\n .y-shift-control {\n display: flex;\n flex: 0;\n\n &[disabled=\"disabled\"] * {\n opacity: 0.5;\n }\n }\n\n .x-shift-control {\n align-items: flex-start;\n }\n\n .x-shift-control .wrap,\n input[type=\"range\"] {\n margin: 0;\n width: $side;\n height: 2em;\n }\n\n .y-shift-control {\n flex-direction: column;\n align-items: flex-end;\n\n .wrap {\n width: 2em;\n height: $side;\n }\n\n input[type=\"range\"] {\n transform-origin: 1em 1em;\n transform: rotate(90deg);\n }\n }\n\n .preview-window {\n flex: 1;\n background-color: #999;\n display: flex;\n align-items: center;\n justify-content: center;\n background-image:\n linear-gradient(45deg, #666 25%, transparent 25%),\n linear-gradient(-45deg, #666 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #666 75%),\n linear-gradient(-45deg, transparent 75%, #666 75%);\n background-size: 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .preview-block {\n width: 33%;\n height: 33%;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n }\n\n .shadow-tweak {\n flex: 1;\n min-width: 280px;\n\n .id-control {\n align-items: stretch;\n\n .shadow-switcher {\n flex: 1;\n }\n\n .shadow-switcher,\n .btn {\n min-width: 1px;\n margin-right: 5px;\n }\n\n .btn {\n padding: 0 0.4em;\n margin: 0 0.1em;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.font-control {\n input.custom-font {\n min-width: 10em;\n }\n\n &.custom {\n /* TODO Should make proper joiners... */\n .font-switcher {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .custom-font {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n","\n.contrast-ratio {\n display: flex;\n justify-content: flex-end;\n margin-top: -4px;\n margin-bottom: 5px;\n\n .label {\n margin-right: 1em;\n }\n\n .rating {\n display: inline-block;\n text-align: center;\n margin-left: 0.5em;\n }\n}\n","\n.preview-container {\n position: relative;\n}\n\n.underlay-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 10px;\n right: 10px;\n}\n","@import \"src/variables\";\n\n.theme-tab {\n padding-bottom: 2em;\n\n .preset-switcher {\n margin-right: 1em;\n }\n\n .btn {\n margin-left: 0.25em;\n margin-right: 0.25em;\n }\n\n .style-control {\n display: flex;\n align-items: baseline;\n margin-bottom: 5px;\n\n .label {\n flex: 1;\n }\n\n .opt {\n margin: 0.5em;\n }\n\n .color-input {\n flex: 0 0 0;\n }\n\n input,\n select {\n min-width: 3em;\n margin: 0;\n flex: 0;\n\n &[type=\"number\"] {\n min-width: 5em;\n }\n\n &[type=\"range\"] {\n flex: 1;\n min-width: 3em;\n align-self: flex-start;\n }\n }\n\n &.disabled {\n input,\n select {\n opacity: 0.5;\n }\n }\n }\n\n .reset-container {\n flex-wrap: wrap;\n }\n\n .fonts-container,\n .reset-container,\n .apply-container,\n .radius-container,\n .color-container, {\n display: flex;\n }\n\n .fonts-container,\n .radius-container {\n flex-direction: column;\n }\n\n .color-container {\n > h4 {\n width: 99%;\n }\n\n flex-wrap: wrap;\n justify-content: space-between;\n }\n\n .fonts-container,\n .color-container,\n .shadow-container,\n .radius-container,\n .presets-container {\n margin: 1em 1em 0;\n }\n\n .tab-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n width: 100%;\n min-height: 30px;\n margin-bottom: 1em;\n\n p {\n flex: 1;\n margin: 0;\n margin-right: 0.5em;\n }\n }\n\n .tab-header-buttons {\n display: flex;\n flex-direction: column;\n\n .btn {\n min-width: 1px;\n flex: 0 auto;\n padding: 0 1em;\n margin-bottom: 0.5em;\n }\n }\n\n .shadow-selector {\n .override {\n flex: 1;\n margin-left: 0.5em;\n }\n\n .select-container {\n margin-top: -4px;\n margin-bottom: -3px;\n }\n }\n\n .save-load,\n .save-load-options {\n display: flex;\n justify-content: center;\n align-items: baseline;\n flex-wrap: wrap;\n\n .presets,\n .import-export {\n margin-bottom: 0.5em;\n }\n\n .import-export {\n display: flex;\n }\n\n .override {\n margin-left: 0.5em;\n }\n }\n\n .save-load-options {\n flex-wrap: wrap;\n margin-top: 0.5em;\n justify-content: center;\n\n .keep-option {\n margin: 0 0.5em 0.5em;\n min-width: 25%;\n }\n }\n\n .preview-container {\n border-top: 1px dashed;\n border-bottom: 1px dashed;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n margin: 1em 0;\n padding: 1em;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-size: cover;\n background-position: 50% 50%;\n\n .dummy {\n .post {\n font-family: var(--postFont);\n display: flex;\n\n .content {\n flex: 1;\n\n h4 {\n margin-bottom: 0.25em;\n }\n\n .icons {\n margin-top: 0.5em;\n display: flex;\n\n i {\n margin-right: 1em;\n }\n }\n }\n }\n\n .after-post {\n margin-top: 1em;\n display: flex;\n align-items: center;\n }\n\n .avatar,\n .avatar-alt {\n background:\n linear-gradient(\n 135deg,\n #b8e1fc 0%,\n #a9d2f3 10%,\n #90bae4 25%,\n #90bcea 37%,\n #90bff0 50%,\n #6ba8e5 51%,\n #a2daf5 83%,\n #bdf3fd 100%\n );\n color: black;\n font-family: sans-serif;\n text-align: center;\n margin-right: 1em;\n }\n\n .avatar-alt {\n flex: 0 auto;\n margin-left: 28px;\n font-size: 12px;\n min-width: 20px;\n min-height: 20px;\n line-height: 20px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n flex: 0 auto;\n width: 48px;\n height: 48px;\n font-size: 14px;\n line-height: 48px;\n }\n\n .actions {\n display: flex;\n align-items: baseline;\n\n .checkbox {\n display: inline-flex;\n align-items: baseline;\n margin-right: 1em;\n flex: 1;\n }\n }\n\n .separator {\n margin: 1em;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .btn {\n min-width: 3em;\n }\n }\n }\n\n .radius-item {\n flex-basis: auto;\n }\n\n .radius-item,\n .color-item {\n min-width: 20em;\n margin: 5px 6px 0 0;\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n\n &.wide {\n min-width: 60%;\n }\n\n &:not(.wide):nth-child(2n+1) {\n margin-right: 7px;\n }\n\n .color,\n .opacity {\n display: flex;\n align-items: baseline;\n }\n }\n\n .theme-radius-rn,\n .theme-color-cl {\n border: 0;\n box-shadow: none;\n background: transparent;\n color: var(--faint, $fallback--faint);\n align-self: stretch;\n }\n\n .theme-color-cl,\n .theme-radius-in,\n .theme-color-in {\n margin-left: 4px;\n }\n\n .theme-radius-in {\n min-width: 1em;\n max-width: 7em;\n flex: 1;\n }\n\n .theme-radius-lb {\n max-width: 50em;\n }\n\n .theme-preview-content {\n padding: 20px;\n }\n\n .theme-warning {\n display: flex;\n align-items: baseline;\n margin-bottom: 0.5em;\n\n .buttons {\n .btn {\n margin-bottom: 0.5em;\n }\n }\n }\n}\n\n.extra-content {\n .apply-container {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n flex-grow: 1;\n\n /* stylelint-disable-next-line no-descending-specificity */\n .btn {\n flex-grow: 1;\n min-height: 2em;\n min-width: 0;\n max-width: 10em;\n padding: 0;\n }\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/9801.cfe503d4c949ae0c3813.css b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css
new file mode 100644
index 000000000..b27df4a19
Binary files /dev/null and b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css differ
diff --git a/priv/static/static/css/9801.cfe503d4c949ae0c3813.css.map b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css.map
new file mode 100644
index 000000000..7ab561567
--- /dev/null
+++ b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/9801.cfe503d4c949ae0c3813.css","mappings":"AACA,mBACE,qBACA,kBAGF,kBACE,gBACA,eACA,kBCRF,yBACE,qBACA,kBAGF,wBACE,gBACA,eACA,kBCRF,cACE,qBACA,kBAEA,8BACE,iBAIJ,eACE,gBACA,eACA,kBCXA,+BACE,cAEA,YACA,mBAFA,UAEA,CAGF,qCAEE,aACA,sBAFA,gBAGA,WAGF,6BACE,mBAEA,uEAEE,WCpBJ,2BACE,UAGF,kBAEE,iBAGA,eADA,kBAHA,uBAEA,kBAEA,CCRJ,uBACE,YAEA,qCACE,0CACA,qBACA,qBAEA,oFAEE,cACA,mBAEA,0GACE,gBAIJ,sDACE,aAEA,mEACE,SACA,kBAIJ,gDACE,mBAEA,kBADA,gBACA,CAGF,4CACE,eAGF,8CAGE,aADA,eADA,UAEA,CAGF,wGAEE,sBACA,SCnCW","sources":["webpack://pleroma_fe/./src/components/settings_modal/helpers/modified_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/profile_setting_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/draft_buttons.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/attachment_setting.vue","webpack://pleroma_fe/./src/components/settings_modal/admin_tabs/frontends_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/settings_modal_admin_content.scss","webpack://pleroma_fe/./src/_variables.scss"],"sourcesContent":["\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ProfileSettingIndicator {\n display: inline-block;\n position: relative;\n}\n\n.profilesetting-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.DraftButtons {\n display: inline-block;\n position: relative;\n\n .button-default {\n margin-left: 0.5em;\n }\n}\n\n.draft-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.AttachmentSetting {\n .attachment {\n display: block;\n width: 100%;\n height: 15em;\n margin-bottom: 0.5em;\n }\n\n .attachment-input {\n margin-left: 1em;\n display: flex;\n flex-direction: column;\n width: 20em;\n }\n\n .controls {\n margin-bottom: 0.5em;\n\n input,\n button {\n width: 100%;\n }\n }\n}\n",".frontends-tab {\n .cards-list {\n padding: 0;\n }\n\n dd {\n text-overflow: ellipsis;\n word-wrap: nowrap;\n white-space: nowrap;\n overflow-x: hidden;\n max-width: 10em;\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.7d2d223f75c3a14b0991.css b/priv/static/static/css/app.7d2d223f75c3a14b0991.css
deleted file mode 100644
index d79cf910f..000000000
Binary files a/priv/static/static/css/app.7d2d223f75c3a14b0991.css and /dev/null differ
diff --git a/priv/static/static/css/app.7d2d223f75c3a14b0991.css.map b/priv/static/static/css/app.7d2d223f75c3a14b0991.css.map
deleted file mode 100644
index ce9a6fa12..000000000
--- a/priv/static/static/css/app.7d2d223f75c3a14b0991.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./src/components/rich_content/rich_content.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACnDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.7d2d223f75c3a14b0991.css","sourcesContent":[".RichContent blockquote {\n margin: 0.2em 0 0.2em 2em;\n font-style: italic;\n}\n.RichContent pre {\n overflow: auto;\n}\n.RichContent code,\n.RichContent samp,\n.RichContent kbd,\n.RichContent var,\n.RichContent pre {\n font-family: var(--postCodeFont, monospace);\n}\n.RichContent p {\n margin: 0 0 1em 0;\n}\n.RichContent p:last-child {\n margin: 0 0 0 0;\n}\n.RichContent h1 {\n font-size: 1.1em;\n line-height: 1.2em;\n margin: 1.4em 0;\n}\n.RichContent h2 {\n font-size: 1.1em;\n margin: 1em 0;\n}\n.RichContent h3 {\n font-size: 1em;\n margin: 1.2em 0;\n}\n.RichContent h4 {\n margin: 1.1em 0;\n}\n.RichContent .img {\n display: inline-block;\n}\n.RichContent .emoji {\n display: inline-block;\n width: var(--emoji-size, 32px);\n height: var(--emoji-size, 32px);\n}\n.RichContent .img,\n.RichContent video {\n max-width: 100%;\n max-height: 400px;\n vertical-align: middle;\n -o-object-fit: contain;\n object-fit: contain;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n margin: 0.2em auto;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 4em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.c18a2c80794a1b699a61.css b/priv/static/static/css/app.c18a2c80794a1b699a61.css
new file mode 100644
index 000000000..9d523427e
Binary files /dev/null and b/priv/static/static/css/app.c18a2c80794a1b699a61.css differ
diff --git a/priv/static/static/css/app.c18a2c80794a1b699a61.css.map b/priv/static/static/css/app.c18a2c80794a1b699a61.css.map
new file mode 100644
index 000000000..0660dd7a0
--- /dev/null
+++ b/priv/static/static/css/app.c18a2c80794a1b699a61.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"static/css/app.c18a2c80794a1b699a61.css","mappings":"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","sources":["webpack://pleroma_fe/./src/components/modal/modal.vue","webpack://pleroma_fe/./node_modules/vue-virtual-scroller/dist/vue-virtual-scroller.css","webpack://pleroma_fe/./src/components/login_form/login_form.vue","webpack://pleroma_fe/./src/components/media_upload/media_upload.vue","webpack://pleroma_fe/./src/components/scope_selector/scope_selector.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/checkbox/checkbox.vue","webpack://pleroma_fe/./src/components/popover/popover.vue","webpack://pleroma_fe/./src/components/still-image/still-image.vue","webpack://pleroma_fe/./src/components/emoji_picker/emoji_picker.scss","webpack://pleroma_fe/./src/components/emoji_input/emoji_input.vue","webpack://pleroma_fe/./src/components/select/select.vue","webpack://pleroma_fe/./src/components/poll/poll_form.vue","webpack://pleroma_fe/./src/components/flash/flash.vue","webpack://pleroma_fe/./src/components/attachment/attachment.scss","webpack://pleroma_fe/./src/components/gallery/gallery.vue","webpack://pleroma_fe/./src/components/user_avatar/user_avatar.vue","webpack://pleroma_fe/./src/components/mention_link/mention_link.scss","webpack://pleroma_fe/./src/components/mentions_line/mentions_line.scss","webpack://pleroma_fe/./src/components/hashtag_link/hashtag_link.scss","webpack://pleroma_fe/./src/components/rich_content/rich_content.scss","webpack://pleroma_fe/./src/components/poll/poll.vue","webpack://pleroma_fe/./src/components/status_body/status_body.scss","webpack://pleroma_fe/./src/components/link-preview/link-preview.vue","webpack://pleroma_fe/./src/components/status_content/status_content.vue","webpack://pleroma_fe/./src/components/post_status_form/post_status_form.vue","webpack://pleroma_fe/./src/components/remote_follow/remote_follow.vue","webpack://pleroma_fe/./src/components/dialog_modal/dialog_modal.vue","webpack://pleroma_fe/./src/components/moderation_tools/moderation_tools.vue","webpack://pleroma_fe/./src/components/account_actions/account_actions.vue","webpack://pleroma_fe/./src/components/user_note/user_note.vue","webpack://pleroma_fe/./src/components/user_card/user_card.scss","webpack://pleroma_fe/./src/components/user_panel/user_panel.vue","webpack://pleroma_fe/./src/components/navigation/navigation_entry.vue","webpack://pleroma_fe/./src/components/navigation/navigation_pins.vue","webpack://pleroma_fe/./src/components/nav_panel/nav_panel.vue","webpack://pleroma_fe/./src/components/features_panel/features_panel.vue","webpack://pleroma_fe/./src/components/who_to_follow_panel/who_to_follow_panel.vue","webpack://pleroma_fe/./src/components/shout_panel/shout_panel.vue","webpack://pleroma_fe/./src/components/media_modal/media_modal.vue","webpack://pleroma_fe/./src/components/side_drawer/side_drawer.vue","webpack://pleroma_fe/./src/components/mobile_post_status_button/mobile_post_status_button.vue","webpack://pleroma_fe/./src/components/reply_button/reply_button.vue","webpack://pleroma_fe/./src/components/favorite_button/favorite_button.vue","webpack://pleroma_fe/./src/components/react_button/react_button.vue","webpack://pleroma_fe/./src/components/retweet_button/retweet_button.vue","webpack://pleroma_fe/./src/components/extra_buttons/extra_buttons.vue","webpack://pleroma_fe/./src/components/avatar_list/avatar_list.vue","webpack://pleroma_fe/./src/components/status_popover/status_popover.vue","webpack://pleroma_fe/./src/components/user_list_popover/user_list_popover.vue","webpack://pleroma_fe/./src/components/emoji_reactions/emoji_reactions.vue","webpack://pleroma_fe/./src/components/status/status.scss","webpack://pleroma_fe/./src/components/report/report.scss","webpack://pleroma_fe/./src/components/notification/notification.scss","webpack://pleroma_fe/./src/components/notifications/notifications.scss","webpack://pleroma_fe/./src/components/mobile_nav/mobile_nav.vue","webpack://pleroma_fe/./src/components/search_bar/search_bar.vue","webpack://pleroma_fe/./src/components/desktop_nav/desktop_nav.scss","webpack://pleroma_fe/./src/components/list/list.vue","webpack://pleroma_fe/./src/components/user_reporting_modal/user_reporting_modal.vue","webpack://pleroma_fe/./src/components/edit_status_modal/edit_status_modal.vue","webpack://pleroma_fe/./src/components/post_status_modal/post_status_modal.vue","webpack://pleroma_fe/./src/components/status_history_modal/status_history_modal.vue","webpack://pleroma_fe/./src/components/global_notice_list/global_notice_list.vue","webpack://pleroma_fe/./src/App.scss","webpack://pleroma_fe/./src/panel.scss","webpack://pleroma_fe/./src/components/thread_tree/thread_tree.vue","webpack://pleroma_fe/./src/components/conversation/conversation.vue","webpack://pleroma_fe/./src/components/timeline_menu/timeline_menu.vue","webpack://pleroma_fe/./src/components/timeline/timeline.scss","webpack://pleroma_fe/./src/components/tab_switcher/tab_switcher.scss","webpack://pleroma_fe/./src/components/chat_title/chat_title.vue","webpack://pleroma_fe/./src/components/chat_list_item/chat_list_item.scss","webpack://pleroma_fe/./src/components/basic_user_card/basic_user_card.vue","webpack://pleroma_fe/./src/components/chat_new/chat_new.scss","webpack://pleroma_fe/./src/components/chat_list/chat_list.vue","webpack://pleroma_fe/./src/components/chat_message/chat_message.scss","webpack://pleroma_fe/./src/components/chat/chat.scss","webpack://pleroma_fe/./src/components/follow_card/follow_card.vue","webpack://pleroma_fe/./src/hocs/with_load_more/with_load_more.scss","webpack://pleroma_fe/./src/components/user_profile/user_profile.vue","webpack://pleroma_fe/./src/components/search/search.vue","webpack://pleroma_fe/./src/components/interface_language_switcher/interface_language_switcher.vue","webpack://pleroma_fe/./src/components/registration/registration.vue","webpack://pleroma_fe/./src/components/password_reset/password_reset.vue","webpack://pleroma_fe/./src/components/follow_request_card/follow_request_card.vue","webpack://pleroma_fe/./src/components/terms_of_service_panel/terms_of_service_panel.vue","webpack://pleroma_fe/./src/components/staff_panel/staff_panel.vue","webpack://pleroma_fe/./src/components/mrf_transparency_panel/mrf_transparency_panel.scss","webpack://pleroma_fe/./src/components/lists_card/lists_card.vue","webpack://pleroma_fe/./src/components/lists/lists.vue","webpack://pleroma_fe/./src/components/lists_user_search/lists_user_search.vue","webpack://pleroma_fe/./src/components/panel_loading/panel_loading.vue","webpack://pleroma_fe/./src/components/lists_edit/lists_edit.vue","webpack://pleroma_fe/./src/components/announcement_editor/announcement_editor.vue","webpack://pleroma_fe/./src/components/announcement/announcement.vue","webpack://pleroma_fe/./src/components/announcements_page/announcements_page.vue"],"sourcesContent":["\n.modal-view {\n z-index: var(--ZI_modals);\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow: auto;\n pointer-events: none;\n animation-duration: 0.2s;\n animation-name: modal-background-fadein;\n opacity: 0;\n\n > * {\n pointer-events: initial;\n }\n\n &.modal-background {\n pointer-events: initial;\n background-color: rgb(0 0 0 / 50%);\n }\n\n &.open {\n opacity: 1;\n }\n}\n\n@keyframes modal-background-fadein {\n from {\n background-color: rgb(0 0 0 / 0%);\n }\n\n to {\n background-color: rgb(0 0 0 / 50%);\n }\n}\n",".vue-recycle-scroller{position:relative}.vue-recycle-scroller.direction-vertical:not(.page-mode){overflow-y:auto}.vue-recycle-scroller.direction-horizontal:not(.page-mode){overflow-x:auto}.vue-recycle-scroller.direction-horizontal{display:flex}.vue-recycle-scroller__slot{flex:auto 0 0}.vue-recycle-scroller__item-wrapper{flex:1;box-sizing:border-box;overflow:hidden;position:relative}.vue-recycle-scroller.ready .vue-recycle-scroller__item-view{position:absolute;top:0;left:0;will-change:transform}.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper{width:100%}.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper{height:100%}.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view{width:100%}.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view{height:100%}.resize-observer[data-v-b329ee4c]{position:absolute;top:0;left:0;z-index:-1;width:100%;height:100%;border:none;background-color:transparent;pointer-events:none;display:block;overflow:hidden;opacity:0}.resize-observer[data-v-b329ee4c] object{display:block;position:absolute;top:0;left:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1}","\n@import \"../../variables\";\n\n.login-form {\n display: flex;\n flex-direction: column;\n padding: 0.6em;\n\n .btn {\n min-height: 2em;\n width: 10em;\n }\n\n .register {\n flex: 1 1;\n }\n\n .login-bottom {\n margin-top: 1em;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n padding: 0.3em 0.5em 0.6em;\n line-height: 24px;\n }\n\n .form-bottom {\n display: flex;\n padding: 0.5em;\n height: 32px;\n\n button {\n width: 10em;\n }\n\n p {\n margin: 0.35em;\n padding: 0.35em;\n display: flex;\n }\n }\n\n .error {\n text-align: center;\n animation-name: shakeError;\n animation-duration: 0.4s;\n animation-timing-function: ease-in-out;\n }\n}\n","\n@import \"../../variables\";\n\n.media-upload {\n .hidden-input-file {\n display: none;\n }\n}\n\nlabel.media-upload {\n cursor: pointer; // We use
for interactivity... i wonder if it's fine\n}\n","\n@import \"../../variables\";\n\n.ScopeSelector {\n .scope {\n display: inline-block;\n cursor: pointer;\n min-width: 1.3em;\n min-height: 1.3em;\n text-align: center;\n\n &.selected svg {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.checkbox {\n position: relative;\n display: inline-block;\n min-height: 1.2em;\n\n &-indicator {\n position: relative;\n padding-left: 1.2em;\n }\n\n &-indicator::before {\n position: absolute;\n right: 0;\n top: 0;\n display: block;\n content: \"✓\";\n transition: color 200ms;\n width: 1.1em;\n height: 1.1em;\n border-radius: $fallback--checkboxRadius;\n border-radius: var(--checkboxRadius, $fallback--checkboxRadius);\n box-shadow: 0 0 2px black inset;\n box-shadow: var(--inputShadow);\n background-color: $fallback--fg;\n background-color: var(--input, $fallback--fg);\n vertical-align: top;\n text-align: center;\n line-height: 1.1em;\n font-size: 1.1em;\n color: transparent;\n overflow: hidden;\n box-sizing: border-box;\n }\n\n &.disabled {\n .checkbox-indicator::before,\n .label {\n opacity: 0.5;\n }\n\n .label {\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n }\n }\n\n input[type=\"checkbox\"] {\n &:checked + .checkbox-indicator::before {\n color: $fallback--text;\n color: var(--inputText, $fallback--text);\n }\n\n &:indeterminate + .checkbox-indicator::before {\n content: \"–\";\n color: $fallback--text;\n color: var(--inputText, $fallback--text);\n }\n }\n\n &.indeterminate-fix {\n input[type=\"checkbox\"] + .checkbox-indicator::before {\n content: \"–\";\n }\n }\n\n & > span {\n margin-left: 0.5em;\n }\n}\n","\n@import \"../../variables\";\n\n.popover-trigger-button {\n display: inline-block;\n}\n\n.popover {\n z-index: var(--ZI_popover_override, var(--ZI_popovers));\n position: fixed;\n min-width: 0;\n max-width: calc(100vw - 20px);\n box-shadow: 2px 2px 3px rgb(0 0 0 / 50%);\n box-shadow: var(--popupShadow);\n}\n\n.popover-default {\n &::after {\n content: \"\";\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 3;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n pointer-events: none;\n }\n\n border-radius: $fallback--btnRadius;\n border-radius: var(--btnRadius, $fallback--btnRadius);\n background-color: $fallback--bg;\n background-color: var(--popover, $fallback--bg);\n color: $fallback--text;\n color: var(--popoverText, $fallback--text);\n\n --faint: var(--popoverFaintText, $fallback--faint);\n --faintLink: var(--popoverFaintLink, $fallback--faint);\n --lightText: var(--popoverLightText, $fallback--lightText);\n --postLink: var(--popoverPostLink, $fallback--link);\n --postFaintLink: var(--popoverPostFaintLink, $fallback--link);\n --icon: var(--popoverIcon, $fallback--icon);\n}\n\n.dropdown-menu {\n display: block;\n padding: 0.5rem 0;\n font-size: 1em;\n text-align: left;\n list-style: none;\n max-width: 100vw;\n z-index: var(--ZI_popover_override, var(--ZI_popovers));\n white-space: nowrap;\n\n .dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid $fallback--border;\n border-top: 1px solid var(--border, $fallback--border);\n }\n\n .dropdown-item {\n line-height: 21px;\n overflow: hidden;\n display: block;\n padding: 0.5em 0.75em;\n clear: both;\n font-weight: 400;\n text-align: inherit;\n white-space: nowrap;\n border: none;\n border-radius: 0;\n background-color: transparent;\n box-shadow: none;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n\n --btnText: var(--popoverText, $fallback--text);\n\n &-icon {\n svg {\n width: 22px;\n margin-right: 0.75rem;\n color: var(--menuPopoverIcon, $fallback--icon);\n }\n }\n\n &.-has-submenu {\n .chevron-icon {\n margin-right: 0.25rem;\n margin-left: 2rem;\n }\n }\n\n &:active,\n &:hover {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenuPopover, $fallback--lightBg);\n box-shadow: none;\n\n --btnText: var(--selectedMenuPopoverText, $fallback--link);\n --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);\n --icon: var(--selectedMenuPopoverIcon, $fallback--icon);\n\n svg {\n color: var(--selectedMenuPopoverIcon, $fallback--icon);\n\n --icon: var(--selectedMenuPopoverIcon, $fallback--icon);\n }\n }\n\n .menu-checkbox {\n display: inline-block;\n vertical-align: middle;\n min-width: 22px;\n max-width: 22px;\n min-height: 22px;\n max-height: 22px;\n line-height: 22px;\n text-align: center;\n border-radius: 0;\n background-color: $fallback--fg;\n background-color: var(--input, $fallback--fg);\n box-shadow: 0 0 2px black inset;\n box-shadow: var(--inputShadow);\n margin-right: 0.75em;\n\n &.menu-checkbox-checked::after {\n font-size: 1.25em;\n content: \"✓\";\n }\n\n &.-radio {\n border-radius: 9999px;\n\n &.menu-checkbox-checked::after {\n font-size: 2em;\n content: \"•\";\n }\n }\n }\n }\n\n .button-default.dropdown-item {\n &,\n i[class*=\"icon-\"] {\n color: $fallback--text;\n color: var(--btnText, $fallback--text);\n }\n\n &:active {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenuPopover, $fallback--lightBg);\n color: $fallback--link;\n color: var(--selectedMenuPopoverText, $fallback--link);\n }\n\n &:disabled {\n color: $fallback--text;\n color: var(--btnDisabledText, $fallback--text);\n }\n\n &.toggled {\n color: $fallback--text;\n color: var(--btnToggledText, $fallback--text);\n }\n }\n}\n","\n@import \"../../variables\";\n\n.still-image {\n position: relative;\n line-height: 0;\n overflow: hidden;\n display: inline-flex;\n align-items: center;\n\n canvas {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n visibility: var(--_still-image-canvas-visibility, visible);\n }\n\n img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n }\n\n &.animated {\n &::before {\n zoom: var(--_still_image-label-scale, 1);\n content: \"gif\";\n position: absolute;\n line-height: 1;\n font-size: 0.7em;\n top: 0.5em;\n left: 0.5em;\n background: rgb(127 127 127 / 50%);\n color: #fff;\n display: block;\n padding: 2px 4px;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n z-index: 2;\n visibility: var(--_still-image-label-visibility, visible);\n }\n\n &:hover canvas {\n display: none;\n }\n\n &:hover::before {\n visibility: var(--_still-image-label-visibility, hidden);\n }\n\n img {\n visibility: var(--_still-image-img-visibility, hidden);\n }\n\n &:hover img {\n visibility: visible;\n }\n }\n}\n","@import \"../../variables\";\n\n$emoji-picker-header-height: 36px;\n$emoji-picker-header-picture-width: 32px;\n$emoji-picker-header-picture-height: 32px;\n$emoji-picker-emoji-size: 32px;\n\n.emoji-picker {\n width: 25em;\n max-width: calc(100vw - 20px); // popover gives 10px margin from window edge\n display: flex;\n flex-direction: column;\n background-color: $fallback--bg;\n background-color: var(--popover, $fallback--bg);\n color: $fallback--link;\n color: var(--popoverText, $fallback--link);\n\n --faint: var(--popoverFaintText, $fallback--faint);\n --faintLink: var(--popoverFaintLink, $fallback--faint);\n --lightText: var(--popoverLightText, $fallback--lightText);\n --icon: var(--popoverIcon, $fallback--icon);\n\n &-header-image {\n display: inline-flex;\n justify-content: center;\n align-items: center;\n width: $emoji-picker-header-picture-width;\n max-width: $emoji-picker-header-picture-width;\n height: $emoji-picker-header-picture-height;\n max-height: $emoji-picker-header-picture-height;\n\n .still-image {\n max-width: 100%;\n max-height: 100%;\n height: 100%;\n width: 100%;\n object-fit: contain;\n }\n }\n\n .keep-open,\n .too-many-emoji {\n padding: 7px;\n line-height: normal;\n }\n\n .too-many-emoji {\n display: flex;\n flex-direction: column;\n }\n\n .keep-open-label {\n padding: 0 7px;\n display: flex;\n }\n\n .heading {\n display: flex;\n padding: 10px 7px 5px;\n }\n\n .content {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-height: 0;\n }\n\n .emoji-tabs {\n flex-grow: 1;\n display: flex;\n flex-flow: row nowrap;\n overflow-x: auto;\n }\n\n .additional-tabs {\n display: flex;\n border-left: 1px solid;\n border-left-color: $fallback--icon;\n border-left-color: var(--icon, $fallback--icon);\n padding-left: 7px;\n flex: 0 0 auto;\n }\n\n .additional-tabs,\n .emoji-tabs {\n flex-basis: auto;\n display: flex;\n align-content: center;\n\n &-item {\n padding: 0 7px;\n cursor: pointer;\n font-size: 1.85em;\n width: $emoji-picker-header-picture-width;\n max-width: $emoji-picker-header-picture-width;\n height: $emoji-picker-header-picture-height;\n max-height: $emoji-picker-header-picture-height;\n display: flex;\n align-items: center;\n\n &.disabled {\n opacity: 0.5;\n pointer-events: none;\n }\n\n &.active {\n border-bottom: 4px solid;\n\n svg {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n }\n }\n\n .sticker-picker {\n flex: 1 1 auto;\n }\n\n .stickers,\n .emoji {\n &-content {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-height: 0;\n\n &.hidden {\n opacity: 0;\n pointer-events: none;\n position: absolute;\n }\n }\n }\n\n .emoji {\n &-search {\n padding: 5px;\n flex: 0 0 auto;\n\n input {\n width: 100%;\n }\n }\n\n &-groups {\n height: 100%;\n min-height: 200px;\n flex: 1 1 1px;\n position: relative;\n overflow: auto;\n user-select: none;\n mask:\n linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,\n linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,\n linear-gradient(to top, white, white);\n transition: mask-size 150ms;\n mask-size: 100% 20px, 100% 20px, auto;\n // Autoprefixed seem to ignore this one, and also syntax is different\n mask-composite: xor;\n mask-composite: exclude;\n\n &.scrolled {\n &-top {\n mask-size: 100% 20px, 100% 0, auto;\n }\n\n &-bottom {\n mask-size: 100% 0, 100% 20px, auto;\n }\n }\n }\n\n &-group {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n padding-left: 5px;\n justify-content: left;\n\n &-title {\n font-size: 0.85em;\n width: 100%;\n margin: 0;\n\n &.disabled {\n display: none;\n }\n }\n }\n\n &-item {\n width: $emoji-picker-emoji-size;\n height: $emoji-picker-emoji-size;\n box-sizing: border-box;\n display: flex;\n line-height: $emoji-picker-emoji-size;\n align-items: center;\n justify-content: center;\n margin: 4px;\n cursor: pointer;\n\n .emoji-picker-emoji.-custom {\n object-fit: contain;\n max-width: 100%;\n max-height: 100%;\n }\n\n .emoji-picker-emoji.-unicode {\n font-size: 24px;\n overflow: hidden;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.emoji-input {\n display: flex;\n flex-direction: column;\n position: relative;\n\n .emoji-picker-icon {\n position: absolute;\n top: 0;\n right: 0;\n margin: 0.2em 0.25em;\n font-size: 1.3em;\n cursor: pointer;\n line-height: 24px;\n\n &:hover i {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n }\n\n .emoji-picker-panel {\n position: absolute;\n z-index: 20;\n margin-top: 2px;\n\n &.hide {\n display: none;\n }\n }\n\n input,\n textarea {\n flex: 1 0 auto;\n }\n\n &.with-picker input {\n padding-right: 30px;\n }\n\n .hidden-overlay {\n opacity: 0;\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n left: 0;\n overflow: hidden;\n\n /* DEBUG STUFF */\n color: red;\n\n /* set opacity to non-zero to see the overlay */\n\n .caret {\n width: 0;\n margin-right: calc(-1ch - 1px);\n border: 1px solid red;\n }\n }\n}\n\n.autocomplete {\n &-panel {\n position: absolute;\n }\n\n &-item {\n display: flex;\n cursor: pointer;\n padding: 0.2em 0.4em;\n border-bottom: 1px solid rgb(0 0 0 / 40%);\n height: 32px;\n\n .image {\n width: 32px;\n height: 32px;\n line-height: 32px;\n text-align: center;\n font-size: 32px;\n margin-right: 4px;\n\n img {\n width: 32px;\n height: 32px;\n object-fit: contain;\n }\n }\n\n .label {\n display: flex;\n flex-direction: column;\n justify-content: center;\n margin: 0 0.1em 0 0.2em;\n\n .displayText {\n line-height: 1.5;\n }\n\n .detailText {\n font-size: 9px;\n line-height: 9px;\n }\n }\n\n &.highlighted {\n background-color: $fallback--fg;\n background-color: var(--selectedMenuPopover, $fallback--fg);\n color: var(--selectedMenuPopoverText, $fallback--text);\n\n --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);\n --icon: var(--selectedMenuPopoverIcon, $fallback--icon);\n }\n }\n}\n","\n@import \"../../variables\";\n\n/* TODO fix order of styles */\nlabel.Select {\n padding: 0;\n\n select {\n appearance: none;\n background: transparent;\n border: none;\n color: $fallback--text;\n color: var(--inputText, --text, $fallback--text);\n margin: 0;\n padding: 0 2em 0 0.2em;\n font-family: sans-serif;\n font-family: var(--inputFont, sans-serif);\n font-size: 1em;\n width: 100%;\n z-index: 1;\n height: 2em;\n line-height: 16px;\n }\n\n .select-down-icon {\n position: absolute;\n top: 0;\n bottom: 0;\n right: 5px;\n height: 100%;\n width: 0.875em;\n color: $fallback--text;\n color: var(--inputText, $fallback--text);\n line-height: 2;\n z-index: 0;\n pointer-events: none;\n }\n}\n","\n@import \"../../variables\";\n\n.poll-form {\n display: flex;\n flex-direction: column;\n padding: 0 0.5em 0.5em;\n\n .add-option {\n align-self: flex-start;\n padding-top: 0.25em;\n padding-left: 0.1em;\n }\n\n .poll-option {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 0.25em;\n }\n\n .input-container {\n width: 100%;\n\n input {\n // Hack: dodge the floating X icon\n padding-right: 2.5em;\n width: 100%;\n }\n }\n\n .delete-option {\n // Hack: Move the icon over the input box\n width: 1.5em;\n margin-left: -1.5em;\n z-index: 1;\n }\n\n .poll-type-expiry {\n margin-top: 0.5em;\n display: flex;\n width: 100%;\n }\n\n .poll-type {\n margin-right: 0.75em;\n flex: 1 1 60%;\n\n .poll-type-select {\n padding-right: 0.75em;\n }\n }\n\n .poll-expiry {\n display: flex;\n\n .expiry-amount {\n width: 3em;\n text-align: right;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.Flash {\n display: inline-block;\n width: 100%;\n height: 100%;\n position: relative;\n\n .player {\n height: 100%;\n width: 100%;\n }\n\n .placeholder {\n height: 100%;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--bg);\n color: var(--link);\n }\n\n .hider {\n top: 0;\n }\n\n .label {\n text-align: center;\n flex: 1 1 0;\n line-height: 1.2;\n white-space: normal;\n word-wrap: normal;\n }\n\n .hidden {\n display: none;\n visibility: \"hidden\";\n }\n}\n","@import \"../../variables\";\n\n.Attachment {\n display: inline-flex;\n flex-direction: column;\n position: relative;\n align-self: flex-start;\n line-height: 0;\n height: 100%;\n border-style: solid;\n border-width: 1px;\n border-radius: $fallback--attachmentRadius;\n border-radius: var(--attachmentRadius, $fallback--attachmentRadius);\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n\n .attachment-wrapper {\n flex: 1 1 auto;\n height: 100%;\n position: relative;\n overflow: hidden;\n }\n\n .description-container {\n flex: 0 1 0;\n display: flex;\n padding-top: 0.5em;\n z-index: 1;\n\n p {\n flex: 1;\n text-align: center;\n line-height: 1.5;\n padding: 0.5em;\n margin: 0;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n }\n\n &.-static {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n padding-top: 0;\n background: var(--popover);\n box-shadow: var(--popupShadow);\n }\n }\n\n .description-field {\n flex: 1;\n min-width: 0;\n }\n\n & .placeholder-container,\n & .image-container,\n & .audio-container,\n & .video-container,\n & .flash-container,\n & .oembed-container {\n display: flex;\n justify-content: center;\n width: 100%;\n height: 100%;\n }\n\n .image-container {\n .image {\n width: 100%;\n height: 100%;\n }\n }\n\n & .flash-container,\n & .video-container {\n & .flash,\n & video {\n width: 100%;\n height: 100%;\n object-fit: contain;\n align-self: center;\n }\n }\n\n .audio-container {\n display: flex;\n align-items: flex-end;\n\n audio {\n width: 100%;\n height: 100%;\n }\n }\n\n .placeholder-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding-top: 0.5em;\n }\n\n .play-icon {\n position: absolute;\n font-size: 64px;\n top: calc(50% - 32px);\n left: calc(50% - 32px);\n color: rgb(255 255 255 / 75%);\n text-shadow: 0 0 2px rgb(0 0 0 / 40%);\n\n &::before {\n margin: 0;\n }\n }\n\n .attachment-buttons {\n display: flex;\n position: absolute;\n right: 0;\n top: 0;\n margin-top: 0.5em;\n margin-right: 0.5em;\n z-index: 1;\n\n .attachment-button {\n padding: 0;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n text-align: center;\n width: 2em;\n height: 2em;\n margin-left: 0.5em;\n font-size: 1.25em;\n // TODO: theming? hard to theme with unknown background image color\n background: rgb(230 230 230 / 70%);\n\n .svg-inline--fa {\n color: rgb(0 0 0 / 60%);\n }\n\n &:hover .svg-inline--fa {\n color: rgb(0 0 0 / 90%);\n }\n }\n }\n\n &.-contain-fit {\n img,\n canvas {\n object-fit: contain;\n }\n }\n\n &.-cover-fit {\n img,\n canvas {\n object-fit: cover;\n }\n }\n\n .oembed-container {\n line-height: 1.2em;\n flex: 1 0 100%;\n width: 100%;\n margin-right: 15px;\n display: flex;\n\n img {\n width: 100%;\n }\n\n .image {\n flex: 1;\n\n img {\n border: 0;\n border-radius: 5px;\n height: 100%;\n object-fit: cover;\n }\n }\n\n .text {\n flex: 2;\n margin: 8px;\n word-break: break-all;\n\n h1 {\n font-size: 1rem;\n margin: 0;\n }\n }\n }\n\n &.-size-small {\n .play-icon {\n zoom: 0.5;\n opacity: 0.7;\n }\n\n .attachment-buttons {\n zoom: 0.7;\n opacity: 0.5;\n }\n }\n\n &.-editable {\n padding: 0.5em;\n\n & .description-container,\n & .attachment-buttons {\n margin: 0;\n }\n }\n\n &.-placeholder {\n display: inline-block;\n color: $fallback--link;\n color: var(--postLink, $fallback--link);\n overflow: hidden;\n white-space: nowrap;\n height: auto;\n line-height: 1.5;\n\n &:not(.-editable) {\n border: none;\n }\n\n &.-editable {\n display: flex;\n flex-direction: row;\n align-items: baseline;\n\n & .description-container,\n & .attachment-buttons {\n margin: 0;\n padding: 0;\n position: relative;\n }\n\n .description-container {\n flex: 1;\n padding-left: 0.5em;\n }\n\n .attachment-buttons {\n order: 99;\n align-self: center;\n }\n }\n\n a {\n display: inline-block;\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n svg {\n color: inherit;\n }\n }\n\n &.-loading {\n cursor: progress;\n }\n\n &.-compact {\n .placeholder-container {\n padding-bottom: 0.5em;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.Gallery {\n .gallery-rows {\n display: flex;\n flex-direction: column;\n }\n\n .gallery-row {\n position: relative;\n height: 0;\n width: 100%;\n flex-grow: 1;\n\n .gallery-row-inner {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n flex-flow: row wrap;\n align-content: stretch;\n\n .gallery-item {\n margin: 0 0.5em 0 0;\n flex-grow: 1;\n height: 100%;\n box-sizing: border-box;\n // to make failed images a bit more noticeable on chromium\n min-width: 2em;\n\n &:last-child {\n margin: 0;\n }\n }\n\n &.-grid {\n width: 100%;\n height: auto;\n position: relative;\n display: grid;\n grid-gap: 0.5em;\n grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));\n\n .gallery-item {\n margin: 0;\n height: 200px;\n }\n }\n }\n\n &.-grid,\n &.-minimal {\n height: auto;\n\n .gallery-row-inner {\n position: relative;\n }\n }\n\n &:not(:first-child) {\n margin-top: 0.5em;\n }\n }\n\n &.-long {\n .gallery-rows {\n max-height: 25em;\n overflow: hidden;\n mask:\n linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,\n linear-gradient(to top, white, white);\n\n /* Autoprefixed seem to ignore this one, and also syntax is different */\n mask-composite: xor;\n mask-composite: exclude;\n }\n }\n\n .many-attachments-text {\n text-align: center;\n line-height: 2;\n }\n\n .many-attachments-buttons {\n display: flex;\n }\n\n .many-attachments-button {\n display: flex;\n flex: 1;\n justify-content: center;\n line-height: 2;\n\n button {\n padding: 0 2em;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.Avatar {\n --_avatarShadowBox: var(--avatarStatusShadow);\n --_avatarShadowFilter: var(--avatarStatusShadowFilter);\n --_avatarShadowInset: var(--avatarStatusShadowInset);\n --_still-image-label-visibility: hidden;\n\n display: inline-block;\n position: relative;\n width: 48px;\n height: 48px;\n\n &.-compact {\n width: 32px;\n height: 32px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n width: 100%;\n height: 100%;\n box-shadow: var(--_avatarShadowBox);\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n\n &.-better-shadow {\n box-shadow: var(--_avatarShadowInset);\n filter: var(--_avatarShadowFilter);\n }\n\n &.-animated::before {\n display: none;\n }\n\n &.-compact {\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n &.-placeholder {\n background-color: $fallback--fg;\n background-color: var(--fg, $fallback--fg);\n }\n }\n\n img {\n width: 100%;\n height: 100%;\n }\n\n .bot-indicator {\n position: absolute;\n bottom: 0;\n right: 0;\n margin: -0.2em;\n padding: 0.2em;\n background: rgb(127 127 127 / 50%);\n color: #fff;\n border-radius: var(--tooltipRadius);\n }\n}\n","@import \"../../variables\";\n\n.MentionLink {\n position: relative;\n white-space: normal;\n display: inline;\n color: var(--link);\n word-break: normal;\n\n & .new,\n & .original {\n display: inline;\n border-radius: 2px;\n }\n\n .mention-avatar {\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n user-select: none;\n margin-right: 0.2em;\n }\n\n .full {\n position: absolute;\n display: inline-block;\n pointer-events: none;\n opacity: 0;\n top: 100%;\n left: 0;\n height: 100%;\n word-wrap: normal;\n white-space: nowrap;\n transition: opacity 0.2s ease;\n z-index: 1;\n margin-top: 0.25em;\n padding: 0.5em;\n user-select: all;\n }\n\n & .short.-with-tooltip,\n & .you {\n user-select: none;\n }\n\n & .short,\n & .full {\n white-space: nowrap;\n }\n\n .shortName {\n white-space: normal;\n }\n\n .new {\n &.-you {\n .shortName {\n font-weight: 600;\n }\n }\n\n &.-has-selection {\n color: var(--alertNeutralText, $fallback--text);\n background-color: var(--alertNeutral, $fallback--fg);\n }\n\n .at {\n color: var(--link);\n opacity: 0.8;\n display: inline-block;\n line-height: 1;\n padding: 0 0.1em;\n vertical-align: -25%;\n margin: 0;\n }\n\n &.-striped {\n & .shortName {\n background-image:\n repeating-linear-gradient(\n 135deg,\n var(--____highlight-tintColor),\n var(--____highlight-tintColor) 5px,\n var(--____highlight-tintColor2) 5px,\n var(--____highlight-tintColor2) 10px\n );\n }\n }\n\n &.-solid {\n .shortName {\n background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));\n }\n }\n\n &.-side {\n .shortName {\n box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);\n }\n }\n }\n\n .serverName.-faded {\n color: var(--faintLink, $fallback--link);\n }\n}\n\n.mention-link-popover {\n max-width: 70ch;\n max-height: 20rem;\n overflow: hidden;\n}\n",".MentionsLine {\n word-break: break-all;\n\n .mention-link:not(:first-child)::before {\n content: \" \";\n }\n\n .showMoreLess {\n margin-left: 0.5em;\n white-space: normal;\n color: var(--link);\n }\n}\n",".HashtagLink {\n position: relative;\n white-space: normal;\n display: inline-block;\n color: var(--link);\n}\n","@import \"../../variables\";\n\n.RichContent {\n blockquote {\n margin: 0.2em 0 0.2em 0.2em;\n font-style: italic;\n border-left: 0.2em solid var(--faint, $fallback--faint);\n padding-left: 1em;\n }\n\n pre {\n overflow: auto;\n }\n\n code,\n samp,\n kbd,\n var,\n pre {\n font-family: var(--postCodeFont, monospace);\n }\n\n p {\n margin: 0 0 1em;\n }\n\n p:last-child {\n margin: 0;\n }\n\n h1 {\n font-size: 1.1em;\n line-height: 1.2em;\n margin: 1.4em 0;\n }\n\n h2 {\n font-size: 1.1em;\n margin: 1em 0;\n }\n\n h3 {\n font-size: 1em;\n margin: 1.2em 0;\n }\n\n h4 {\n margin: 1.1em 0;\n }\n\n .img {\n display: inline-block;\n }\n\n .emoji {\n display: inline-block;\n width: var(--emoji-size, 32px);\n height: var(--emoji-size, 32px);\n }\n\n .img,\n video {\n max-width: 100%;\n max-height: 400px;\n vertical-align: middle;\n object-fit: contain;\n }\n}\n","\n@import \"../../variables\";\n\n.poll {\n .votes {\n display: flex;\n flex-direction: column;\n margin: 0 0 0.5em;\n }\n\n .poll-option {\n margin: 0.75em 0.5em;\n }\n\n .option-result {\n height: 100%;\n display: flex;\n flex-direction: row;\n position: relative;\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n\n .option-result-label {\n display: flex;\n align-items: center;\n padding: 0.1em 0.25em;\n z-index: 1;\n word-break: break-word;\n }\n\n .result-percentage {\n width: 3.5em;\n flex-shrink: 0;\n }\n\n .result-fill {\n height: 100%;\n position: absolute;\n color: $fallback--text;\n color: var(--pollText, $fallback--text);\n background-color: $fallback--lightBg;\n background-color: var(--poll, $fallback--lightBg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n top: 0;\n left: 0;\n transition: width 0.5s;\n }\n\n .option-vote {\n display: flex;\n align-items: center;\n }\n\n input {\n width: 3.5em;\n }\n\n .footer {\n display: flex;\n align-items: center;\n }\n\n &.loading * {\n cursor: progress;\n }\n\n .poll-vote-button {\n padding: 0 0.5em;\n margin-right: 0.5em;\n }\n\n .poll-checkbox {\n display: none;\n }\n}\n","@import \"../../variables\";\n\n.StatusBody {\n display: flex;\n flex-direction: column;\n\n .emoji {\n --_still_image-label-scale: 0.5;\n }\n\n .attachments {\n margin-top: 0.5em;\n }\n\n & .text,\n & .summary {\n font-family: var(--postFont, sans-serif);\n white-space: pre-wrap;\n overflow-wrap: break-word;\n word-wrap: break-word;\n word-break: break-word;\n line-height: var(--post-line-height);\n }\n\n .summary {\n display: block;\n font-style: italic;\n padding-bottom: 0.5em;\n }\n\n .text {\n &.-single-line {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n height: 1.4em;\n }\n }\n\n .summary-wrapper {\n margin-bottom: 0.5em;\n border-style: solid;\n border-width: 0 0 1px;\n border-color: var(--border, $fallback--border);\n flex-grow: 0;\n\n &.-tall {\n position: relative;\n\n .summary {\n max-height: 2em;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n }\n }\n\n .text-wrapper {\n display: flex;\n flex-flow: column nowrap;\n\n &.-tall-status {\n position: relative;\n height: 220px;\n overflow-x: hidden;\n overflow-y: hidden;\n z-index: 1;\n\n .media-body {\n min-height: 0;\n mask:\n linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,\n linear-gradient(to top, white, white);\n\n /* Autoprefixed seem to ignore this one, and also syntax is different */\n mask-composite: xor;\n mask-composite: exclude;\n }\n }\n }\n\n & .tall-status-hider,\n & .tall-subject-hider,\n & .status-unhider,\n & .cw-status-hider {\n display: inline-block;\n word-break: break-all;\n width: 100%;\n text-align: center;\n }\n\n .tall-status-hider {\n position: absolute;\n height: 70px;\n margin-top: 150px;\n line-height: 110px;\n z-index: 2;\n }\n\n .tall-subject-hider {\n // position: absolute;\n padding-bottom: 0.5em;\n }\n\n & .status-unhider,\n & .cw-status-hider {\n word-break: break-all;\n\n svg {\n color: inherit;\n }\n }\n\n .greentext {\n color: $fallback--cGreen;\n color: var(--postGreentext, $fallback--cGreen);\n }\n\n .cyantext {\n color: var(--postCyantext, $fallback--cBlue);\n }\n\n &.-compact {\n align-items: top;\n flex-direction: row;\n\n --emoji-size: 16px;\n\n & .body,\n & .attachments {\n max-height: 3.25em;\n }\n\n .body {\n overflow: hidden;\n white-space: normal;\n min-width: 5em;\n flex: 5 1 auto;\n mask-size: auto 3.5em, auto auto;\n mask-position: 0 0, 0 0;\n mask-repeat: repeat-x, repeat;\n mask-image: linear-gradient(to bottom, white 2em, transparent 3em);\n\n /* Autoprefixed seem to ignore this one, and also syntax is different */\n mask-composite: xor;\n mask-composite: exclude;\n }\n\n .attachments {\n margin-top: 0;\n flex: 1 1 0;\n min-width: 5em;\n height: 100%;\n margin-left: 0.5em;\n }\n\n .summary-wrapper {\n .summary::after {\n content: \": \";\n }\n\n line-height: inherit;\n margin: 0;\n border: none;\n display: inline-block;\n }\n\n .text-wrapper {\n display: inline-block;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.link-preview-card {\n display: flex;\n flex-direction: row;\n cursor: pointer;\n overflow: hidden;\n margin-top: 0.5em;\n\n .card-image {\n flex-shrink: 0;\n width: 120px;\n max-width: 25%;\n\n img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: $fallback--attachmentRadius;\n border-radius: var(--attachmentRadius, $fallback--attachmentRadius);\n }\n }\n\n .card-content {\n max-height: 100%;\n margin: 0.5em;\n display: flex;\n flex-direction: column;\n }\n\n .card-host {\n font-size: 0.85em;\n }\n\n .card-description {\n margin: 0.5em 0 0;\n overflow: hidden;\n text-overflow: ellipsis;\n word-break: break-word;\n line-height: 1.2em;\n // cap description at 3 lines, the 1px is to clean up some stray pixels\n // TODO: fancier fade-out at the bottom to show off that it's too long?\n max-height: calc(1.2em * 3 - 1px);\n }\n\n .nsfw-alert {\n margin: 2em 0;\n }\n\n color: $fallback--text;\n color: var(--text, $fallback--text);\n border-style: solid;\n border-width: 1px;\n border-radius: $fallback--attachmentRadius;\n border-radius: var(--attachmentRadius, $fallback--attachmentRadius);\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n}\n","\n.StatusContent {\n flex: 1;\n min-width: 0;\n}\n","\n@import \"../../variables\";\n\n.post-status-form {\n position: relative;\n\n .attachments {\n margin-bottom: 0.5em;\n }\n\n .form-bottom {\n display: flex;\n justify-content: space-between;\n padding: 0.5em;\n height: 2.5em;\n\n button {\n width: 10em;\n }\n\n p {\n margin: 0.35em;\n padding: 0.35em;\n display: flex;\n }\n }\n\n .form-bottom-left {\n display: flex;\n flex: 1;\n padding-right: 7px;\n margin-right: 7px;\n max-width: 10em;\n }\n\n .preview-heading {\n display: flex;\n padding-left: 0.5em;\n }\n\n .preview-toggle {\n flex: 1;\n cursor: pointer;\n user-select: none;\n\n &:hover {\n text-decoration: underline;\n }\n\n svg,\n i {\n margin-left: 0.2em;\n font-size: 0.8em;\n transform: rotate(90deg);\n }\n }\n\n .preview-container {\n margin-bottom: 1em;\n }\n\n .preview-error {\n font-style: italic;\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n }\n\n .preview-status {\n border: 1px solid $fallback--border;\n border: 1px solid var(--border, $fallback--border);\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n padding: 0.5em;\n margin: 0;\n }\n\n .reply-or-quote-selector {\n margin-bottom: 0.5em;\n }\n\n .text-format {\n .only-format {\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n }\n }\n\n .visibility-tray {\n display: flex;\n justify-content: space-between;\n padding-top: 5px;\n align-items: baseline;\n }\n\n .visibility-notice.edit-warning {\n > :first-child {\n margin-top: 0;\n }\n\n > :last-child {\n margin-bottom: 0;\n }\n }\n\n // Order is not necessary but a good indicator\n .media-upload-icon {\n order: 1;\n justify-content: left;\n }\n\n .emoji-icon {\n order: 2;\n justify-content: center;\n }\n\n .poll-icon {\n order: 3;\n justify-content: right;\n }\n\n .media-upload-icon,\n .poll-icon,\n .emoji-icon {\n font-size: 1.85em;\n line-height: 1.1;\n flex: 1;\n padding: 0 0.1em;\n display: flex;\n align-items: center;\n\n &.selected,\n &:hover {\n // needs to be specific to override icon default color\n svg,\n i,\n label {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n\n &.disabled {\n svg,\n i {\n cursor: not-allowed;\n color: $fallback--icon;\n color: var(--btnDisabledText, $fallback--icon);\n\n &:hover {\n color: $fallback--icon;\n color: var(--btnDisabledText, $fallback--icon);\n }\n }\n }\n }\n\n .error {\n text-align: center;\n }\n\n .media-upload-wrapper {\n margin-right: 0.2em;\n margin-bottom: 0.5em;\n width: 18em;\n\n img,\n video {\n object-fit: contain;\n max-height: 10em;\n }\n\n .video {\n max-height: 10em;\n }\n\n input {\n flex: 1;\n width: 100%;\n }\n }\n\n .status-input-wrapper {\n display: flex;\n position: relative;\n width: 100%;\n flex-direction: column;\n }\n\n .btn[disabled] {\n cursor: not-allowed;\n }\n\n form {\n display: flex;\n flex-direction: column;\n margin: 0.6em;\n position: relative;\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n padding: 0.25em 0.5em 0.5em;\n line-height: 1.85;\n }\n\n .form-post-body {\n // TODO: make a resizable textarea component?\n box-sizing: content-box; // needed for easier computation of dynamic size\n overflow: hidden;\n transition: min-height 200ms 100ms;\n // stock padding + 1 line of text (for counter)\n padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);\n // two lines of text\n height: calc(var(--post-line-height) * 1em);\n min-height: calc(var(--post-line-height) * 1em);\n resize: none;\n\n &.scrollable-form {\n overflow-y: auto;\n }\n }\n\n .main-input {\n position: relative;\n }\n\n .character-counter {\n position: absolute;\n bottom: 0;\n right: 0;\n padding: 0;\n margin: 0 0.5em;\n\n &.error {\n color: $fallback--cRed;\n color: var(--cRed, $fallback--cRed);\n }\n }\n\n @keyframes fade-in {\n from { opacity: 0; }\n to { opacity: 0.6; }\n }\n\n @keyframes fade-out {\n from { opacity: 0.6; }\n to { opacity: 0; }\n }\n\n .drop-indicator {\n position: absolute;\n width: 100%;\n height: 100%;\n font-size: 5em;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0.6;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n border: 2px dashed $fallback--text;\n border: 2px dashed var(--text, $fallback--text);\n }\n}\n","\n.remote-follow {\n max-width: 220px;\n\n .remote-button {\n width: 100%;\n min-height: 2em;\n }\n}\n","\n@import \"../../variables\";\n\n// TODO: unify with other modals.\n.dark-overlay {\n &::before {\n bottom: 0;\n content: \" \";\n display: block;\n cursor: default;\n left: 0;\n position: fixed;\n right: 0;\n top: 0;\n background: rgb(27 31 35 / 50%);\n z-index: 2000;\n }\n}\n\n.dialog-modal.panel {\n top: 0;\n left: 50%;\n max-height: 80vh;\n max-width: 90vw;\n margin: 15vh auto;\n position: fixed;\n transform: translateX(-50%);\n z-index: 2001;\n cursor: default;\n display: block;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n\n .dialog-modal-heading {\n .title {\n text-align: center;\n }\n }\n\n .dialog-modal-content {\n margin: 0;\n padding: 1rem;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n white-space: normal;\n }\n\n .dialog-modal-footer {\n margin: 0;\n padding: 0.5em;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-top: 1px solid $fallback--border;\n border-top: 1px solid var(--border, $fallback--border);\n display: flex;\n justify-content: flex-end;\n\n button {\n width: auto;\n margin-left: 0.5rem;\n }\n }\n}\n\n","\n@import \"../../variables\";\n\n.moderation-tools-popover {\n height: 100%;\n\n .trigger {\n /* stylelint-disable-next-line declaration-no-important */\n display: flex !important;\n height: 100%;\n }\n}\n\n.moderation-tools-button {\n svg,\n i {\n font-size: 0.8em;\n }\n}\n","\n@import \"../../variables\";\n\n.AccountActions {\n .ellipsis-button {\n width: 2.5em;\n margin: -0.5em 0;\n padding: 0.5em 0;\n text-align: center;\n\n &:not(:hover) .icon {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n}\n","\n@import \"../../variables\";\n\n.user-note {\n display: flex;\n flex-direction: column;\n\n .heading {\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 0.75em;\n\n .btn {\n min-width: 95px;\n }\n\n .buttons {\n display: flex;\n flex-direction: row;\n justify-content: right;\n\n .btn {\n margin-left: 0.5em;\n }\n }\n }\n\n .note-text {\n align-self: stretch;\n }\n\n .note-text.-blank {\n font-style: italic;\n color: var(--faint, $fallback--faint);\n }\n}\n","@import \"../../variables\";\n\n.user-card {\n position: relative;\n z-index: 1;\n\n &:hover {\n --_still-image-img-visibility: visible;\n --_still-image-canvas-visibility: hidden;\n --_still-image-label-visibility: hidden;\n }\n\n .panel-heading {\n padding: 0.5em 0;\n text-align: center;\n box-shadow: none;\n background: transparent;\n flex-direction: column;\n align-items: stretch;\n // create new stacking context\n position: relative;\n }\n\n .panel-body {\n word-wrap: break-word;\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n // create new stacking context\n position: relative;\n }\n\n .background-image {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n mask:\n linear-gradient(to top, white, transparent) bottom no-repeat,\n linear-gradient(to top, white, white);\n // Autoprefixer seem to ignore this one, and also syntax is different\n mask-composite: xor;\n mask-composite: exclude;\n background-size: cover;\n mask-size: 100% 60%;\n border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);\n border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);\n border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);\n border-bottom-right-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);\n background-color: var(--profileBg);\n z-index: -2;\n\n &.hide-bio {\n mask-size: 100% 40px;\n }\n }\n\n &-bio {\n text-align: center;\n display: block;\n line-height: 1.3;\n padding: 1em;\n margin: 0;\n\n a {\n color: $fallback--link;\n color: var(--postLink, $fallback--link);\n }\n\n img {\n object-fit: contain;\n vertical-align: middle;\n max-width: 100%;\n max-height: 400px;\n }\n }\n\n &.-rounded-t {\n border-top-left-radius: $fallback--panelRadius;\n border-top-left-radius: var(--panelRadius, $fallback--panelRadius);\n border-top-right-radius: $fallback--panelRadius;\n border-top-right-radius: var(--panelRadius, $fallback--panelRadius);\n\n --__roundnessTop: var(--panelRadius);\n --__roundnessBottom: 0;\n }\n\n &.-rounded {\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n\n --__roundnessTop: var(--panelRadius);\n --__roundnessBottom: var(--panelRadius);\n }\n\n &.-popover {\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n\n --__roundnessTop: var(--tooltipRadius);\n --__roundnessBottom: var(--tooltipRadius);\n }\n\n &.-bordered {\n border-width: 1px;\n border-style: solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n}\n\n.user-info {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n padding: 0 26px;\n\n a {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n\n &:hover {\n color: var(--icon);\n }\n }\n\n .container {\n min-width: 0;\n padding: 16px 0 6px;\n display: flex;\n align-items: flex-start;\n max-height: 56px;\n\n > * {\n min-width: 0;\n }\n\n > a {\n vertical-align: middle;\n display: flex;\n }\n\n .Avatar {\n --_avatarShadowBox: var(--avatarShadow);\n --_avatarShadowFilter: var(--avatarShadowFilter);\n --_avatarShadowInset: var(--avatarShadowInset);\n\n width: 56px;\n height: 56px;\n object-fit: cover;\n }\n }\n\n &-avatar {\n position: relative;\n cursor: pointer;\n\n &.-overlay {\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n background-color: rgb(0 0 0 / 30%);\n display: flex;\n justify-content: center;\n align-items: center;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n opacity: 0;\n transition: opacity 0.2s ease;\n\n svg {\n color: #fff;\n }\n }\n\n &:hover &.-overlay {\n opacity: 1;\n }\n }\n\n .external-link-button,\n .edit-profile-button {\n cursor: pointer;\n width: 2.5em;\n text-align: center;\n margin: -0.5em 0;\n padding: 0.5em 0;\n\n &:not(:hover) .icon {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n\n .bottom-line {\n font-weight: light;\n font-size: 1.1em;\n align-items: baseline;\n\n .lock-icon {\n margin-left: 0.5em;\n }\n\n .user-screen-name {\n min-width: 1px;\n flex: 0 1 auto;\n text-overflow: ellipsis;\n overflow: hidden;\n }\n\n .dailyAvg {\n min-width: 1px;\n flex: 0 0 auto;\n margin-left: 1em;\n font-size: 0.7em;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n\n .user-role {\n flex: none;\n color: $fallback--text;\n color: var(--alertNeutralText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--alertNeutral, $fallback--fg);\n }\n }\n\n .user-summary {\n display: block;\n margin-left: 0.6em;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1 1 0;\n // This is so that text doesn't get overlapped by avatar's shadow if it has\n // big one\n z-index: 1;\n line-height: 2em;\n\n --emoji-size: 1.7em;\n\n .top-line,\n .bottom-line {\n display: flex;\n }\n }\n\n .user-name {\n text-overflow: ellipsis;\n overflow: hidden;\n flex: 1 1 auto;\n margin-right: 1em;\n font-size: 1.1em;\n }\n\n .user-meta {\n margin-bottom: 0.15em;\n display: flex;\n align-items: baseline;\n line-height: 22px;\n flex-wrap: wrap;\n\n .following {\n flex: 1 0 auto;\n margin: 0;\n margin-bottom: 0.25em;\n text-align: left;\n }\n\n .highlighter {\n flex: 0 1 auto;\n display: flex;\n flex-wrap: wrap;\n margin-right: -0.5em;\n align-self: start;\n\n .userHighlightCl {\n padding: 2px 10px;\n flex: 1 0 auto;\n }\n\n .userHighlightSel {\n padding-top: 0;\n padding-bottom: 0;\n flex: 1 0 auto;\n }\n\n .userHighlightText {\n width: 70px;\n flex: 1 0 auto;\n }\n\n .userHighlightCl,\n .userHighlightText,\n .userHighlightSel {\n vertical-align: top;\n margin-right: 0.5em;\n margin-bottom: 0.25em;\n }\n }\n }\n\n .user-interactions {\n position: relative;\n display: flex;\n flex-flow: row wrap;\n margin-right: -0.75em;\n\n > * {\n margin: 0 0.75em 0.6em 0;\n white-space: nowrap;\n min-width: 95px;\n }\n\n button {\n margin: 0;\n }\n }\n\n .user-note {\n margin: 0 0.75em 0.6em 0;\n }\n}\n\n.sidebar .edit-profile-button {\n display: none;\n}\n\n.user-counts {\n display: flex;\n line-height: 16px;\n padding: 0.5em 1.5em 0;\n text-align: center;\n justify-content: space-between;\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n flex-wrap: wrap;\n}\n\n.user-count {\n flex: 1 0 auto;\n padding: 0.5em 0;\n margin: 0 0.5em;\n\n h5 {\n font-size: 1em;\n font-weight: bolder;\n margin: 0 0 0.25em;\n }\n\n /* stylelint-disable-next-line no-descending-specificity */\n a {\n text-decoration: none;\n }\n}\n\n.mute-expiry {\n display: flex;\n flex-direction: row;\n}\n","\n.user-panel .signed-in {\n overflow: visible;\n z-index: 10;\n}\n","\n@import \"../../variables\";\n\n.NavigationEntry {\n display: flex;\n box-sizing: border-box;\n align-items: baseline;\n height: 3.5em;\n line-height: 3.5em;\n padding: 0 1em;\n width: 100%;\n color: $fallback--link;\n color: var(--link, $fallback--link);\n\n .timelines-chevron {\n margin-right: 0;\n }\n\n .main-link {\n flex: 1;\n }\n\n .menu-icon {\n margin-right: 0.8em;\n }\n\n .extra-button {\n width: 3em;\n text-align: center;\n\n &:last-child {\n margin-right: -0.8em;\n }\n }\n\n &:hover {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: $fallback--link;\n color: var(--selectedMenuText, $fallback--link);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n\n .menu-icon {\n --icon: var(--text, $fallback--icon);\n }\n }\n\n &.-active {\n font-weight: bolder;\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: $fallback--text;\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n\n .menu-icon {\n --icon: var(--text, $fallback--icon);\n }\n\n &:hover {\n text-decoration: underline;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.NavigationPins {\n display: flex;\n flex-wrap: wrap;\n overflow: hidden;\n height: 100%;\n\n .alert-dot {\n border-radius: 100%;\n height: 0.5em;\n width: 0.5em;\n position: absolute;\n right: calc(50% - 0.75em);\n top: calc(50% - 0.5em);\n background-color: $fallback--cRed;\n background-color: var(--badgeNotification, $fallback--cRed);\n }\n\n .pinned-item {\n position: relative;\n flex: 1 0 3em;\n min-width: 2em;\n text-align: center;\n overflow: visible;\n box-sizing: border-box;\n height: 100%;\n\n & .svg-inline--fa,\n & .iconLetter {\n margin: 0;\n }\n\n &.router-link-active {\n color: $fallback--text;\n color: var(--panelText, $fallback--text);\n border-bottom: 4px solid;\n\n & .svg-inline--fa,\n & .iconLetter {\n color: inherit;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.NavPanel {\n .panel {\n overflow: hidden;\n box-shadow: var(--panelShadow);\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n li {\n position: relative;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n > li {\n &:first-child .menu-item {\n border-top-right-radius: $fallback--panelRadius;\n border-top-right-radius: var(--panelRadius, $fallback--panelRadius);\n border-top-left-radius: $fallback--panelRadius;\n border-top-left-radius: var(--panelRadius, $fallback--panelRadius);\n }\n\n &:last-child .menu-item {\n border-bottom-right-radius: $fallback--panelRadius;\n border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);\n border-bottom-left-radius: $fallback--panelRadius;\n border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n\n li:last-child {\n border: none;\n }\n\n .navigation-chevron {\n margin-left: 0.8em;\n margin-right: 0.8em;\n font-size: 1.1em;\n }\n\n .timelines-chevron {\n margin-left: 0.8em;\n font-size: 1.1em;\n }\n\n .timelines-background {\n padding: 0 0 0 0.6em;\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .timelines {\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n }\n\n .nav-panel-heading {\n // breaks without a unit\n // stylelint-disable-next-line length-zero-no-unit\n --panel-heading-height-padding: 0px;\n }\n}\n","\n .features-panel li {\n line-height: 24px;\n }\n","\n .who-to-follow * {\n vertical-align: middle;\n }\n\n .who-to-follow img {\n width: 32px;\n height: 32px;\n }\n\n .who-to-follow {\n padding: 0 1em;\n margin: 0;\n }\n\n .who-to-follow-items {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0;\n margin: 1em 0;\n }\n\n .who-to-follow-more {\n padding: 0;\n margin: 1em 0;\n text-align: center;\n }\n","\n@import \"../../variables\";\n\n.floating-shout {\n position: fixed;\n bottom: 0.5em;\n z-index: var(--ZI_popovers);\n max-width: 25em;\n\n &.-left {\n left: 0.5em;\n }\n\n &:not(.-left) {\n right: 0.5em;\n }\n}\n\n.shout-panel {\n .shout-heading {\n cursor: pointer;\n\n .icon {\n color: $fallback--text;\n color: var(--panelText, $fallback--text);\n margin-right: 0.5em;\n }\n\n .title {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n }\n\n .shout-window {\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 20em;\n }\n\n .shout-window-container {\n height: 100%;\n }\n\n .shout-message {\n display: flex;\n padding: 0.2em 0.5em;\n }\n\n .shout-avatar {\n img {\n height: 24px;\n width: 24px;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n margin-right: 0.5em;\n margin-top: 0.25em;\n }\n }\n\n .shout-input {\n display: flex;\n\n textarea {\n flex: 1;\n margin: 0.6em;\n min-height: 3.5em;\n resize: none;\n }\n }\n\n .shout-panel {\n .title {\n display: flex;\n justify-content: space-between;\n }\n }\n}\n","\n$modal-view-button-icon-height: 3em;\n$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2);\n$modal-view-button-icon-width: 3em;\n$modal-view-button-icon-margin: 0.5em;\n\n.media-modal-view {\n @keyframes media-fadein {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n }\n\n .modal-image-container {\n display: flex;\n overflow: hidden;\n align-items: center;\n flex-direction: column;\n max-width: 100%;\n max-height: 100%;\n width: 100%;\n height: 100%;\n flex-grow: 1;\n justify-content: center;\n\n &-inner {\n width: 100%;\n height: 100%;\n flex-grow: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n }\n }\n\n .description,\n .counter {\n /* Hardcoded since background is also hardcoded */\n color: white;\n margin-top: 1em;\n text-shadow: 0 0 10px black, 0 0 10px black;\n padding: 0.2em 2em;\n }\n\n .description {\n flex: 0 0 auto;\n overflow-y: auto;\n min-height: 1em;\n max-width: 500px;\n max-height: 9.5em;\n word-break: break-all;\n }\n\n .modal-image {\n max-width: 100%;\n max-height: 100%;\n image-orientation: from-image; // NOTE: only FF supports this\n animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;\n\n &.loading {\n opacity: 0.5;\n }\n }\n\n .loading-spinner {\n width: 100%;\n height: 100%;\n position: absolute;\n pointer-events: none;\n display: flex;\n justify-content: center;\n align-items: center;\n\n svg {\n color: white;\n }\n }\n\n .modal-view-button {\n border: 0;\n padding: 0;\n opacity: 0;\n box-shadow: none;\n background: none;\n appearance: none;\n overflow: visible;\n cursor: pointer;\n transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);\n height: $modal-view-button-icon-height;\n width: $modal-view-button-icon-width;\n\n .button-icon {\n position: absolute;\n height: $modal-view-button-icon-height;\n width: $modal-view-button-icon-width;\n font-size: 1rem;\n line-height: $modal-view-button-icon-height;\n color: #fff;\n text-align: center;\n background-color: rgb(0 0 0 / 30%);\n }\n }\n\n .modal-view-button-arrow {\n position: absolute;\n display: block;\n top: 50%;\n margin-top: $modal-view-button-icon-half-height;\n width: $modal-view-button-icon-width;\n height: $modal-view-button-icon-height;\n\n .arrow-icon {\n position: absolute;\n top: 0;\n line-height: $modal-view-button-icon-height;\n color: #fff;\n text-align: center;\n background-color: rgb(0 0 0 / 30%);\n }\n\n &--prev {\n left: 0;\n\n .arrow-icon {\n left: $modal-view-button-icon-margin;\n }\n }\n\n &--next {\n right: 0;\n\n .arrow-icon {\n right: $modal-view-button-icon-margin;\n }\n }\n }\n\n .modal-view-button-hide {\n position: absolute;\n top: 0;\n right: 0;\n\n .button-icon {\n top: $modal-view-button-icon-margin;\n right: $modal-view-button-icon-margin;\n }\n }\n}\n\n.modal-view.media-modal-view {\n z-index: var(--ZI_media_modal);\n flex-direction: column;\n\n .modal-view-button-arrow,\n .modal-view-button-hide {\n opacity: 0.75;\n\n &:focus,\n &:hover {\n outline: none;\n box-shadow: none;\n }\n\n &:hover {\n opacity: 1;\n }\n }\n\n overflow: hidden;\n}\n","\n@import \"../../variables\";\n\n.side-drawer-container {\n position: fixed;\n z-index: var(--ZI_navbar);\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: stretch;\n transition-duration: 0s;\n transition-property: transform;\n}\n\n.side-drawer-container-open {\n transform: translate(0%);\n}\n\n.side-drawer-container-closed {\n transition-delay: 0.35s;\n transform: translate(-100%);\n}\n\n.side-drawer-darken {\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n position: fixed;\n z-index: -1;\n transition: 0.35s;\n transition-property: background-color;\n background-color: rgb(0 0 0 / 50%);\n}\n\n.side-drawer-darken-closed {\n background-color: rgb(0 0 0 / 0%);\n}\n\n.side-drawer-click-outside {\n flex: 1 1 100%;\n}\n\n.side-drawer {\n overflow-x: hidden;\n transition: 0.35s;\n transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n transition-property: transform;\n margin: 0 0 0 -100px;\n padding: 0 0 1em 100px;\n width: 80%;\n max-width: 20em;\n flex: 0 0 80%;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n background-color: $fallback--bg;\n background-color: var(--popover, $fallback--bg);\n color: $fallback--link;\n color: var(--popoverText, $fallback--link);\n\n --faint: var(--popoverFaintText, $fallback--faint);\n --faintLink: var(--popoverFaintLink, $fallback--faint);\n --lightText: var(--popoverLightText, $fallback--lightText);\n --icon: var(--popoverIcon, $fallback--icon);\n\n .badge {\n margin-left: 10px;\n }\n}\n\n.side-drawer-logo-wrapper {\n display: flex;\n align-items: center;\n padding: 0.85em;\n\n img {\n flex: none;\n height: 50px;\n margin-right: 0.85em;\n }\n\n span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n}\n\n.side-drawer-click-outside-closed {\n flex: 0 0 0;\n}\n\n.side-drawer-closed {\n transform: translate(-100%);\n}\n\n.side-drawer-heading {\n background: transparent;\n flex-direction: column;\n align-items: stretch;\n display: flex;\n padding: 0;\n margin: 0;\n}\n\n.side-drawer ul {\n list-style: none;\n margin: 0;\n padding: 0;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n}\n\n.side-drawer ul:last-child {\n border: 0;\n}\n\n.side-drawer li {\n padding: 0;\n\n a,\n button {\n box-sizing: border-box;\n display: block;\n height: 3em;\n line-height: 3em;\n padding: 0 0.7em;\n\n &:hover {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenuPopover, $fallback--lightBg);\n color: $fallback--text;\n color: var(--selectedMenuPopoverText, $fallback--text);\n\n --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);\n --icon: var(--selectedMenuPopoverIcon, $fallback--icon);\n }\n }\n}\n","\n@import \"../../variables\";\n\n.MobilePostButton {\n &.button-default {\n width: 5em;\n height: 5em;\n border-radius: 100%;\n position: fixed;\n bottom: 1.5em;\n right: 1.5em;\n // TODO: this needs its own color, it has to stand out enough and link color\n // is not very optimal for this particular use.\n background-color: $fallback--fg;\n background-color: var(--btn, $fallback--fg);\n display: flex;\n justify-content: center;\n align-items: center;\n box-shadow: 0 2px 2px rgb(0 0 0 / 30%), 0 4px 6px rgb(0 0 0 / 30%);\n z-index: 10;\n transition: 0.35s transform;\n transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n }\n\n &.hidden {\n transform: translateY(150%);\n }\n\n svg {\n font-size: 1.5em;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n}\n\n@media all and (min-width: 801px) {\n .new-status-button:not(.always-show) {\n display: none;\n }\n}\n\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.ReplyButton {\n display: flex;\n\n > :first-child {\n padding: 10px;\n margin: -10px -8px -10px -10px;\n }\n\n .action-counter {\n pointer-events: none;\n user-select: none;\n }\n\n .interactive {\n &:hover .svg-inline--fa,\n &.-active .svg-inline--fa {\n color: $fallback--cBlue;\n color: var(--cBlue, $fallback--cBlue);\n }\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n }\n\n @include focused-style {\n .focus-marker {\n visibility: visible;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.FavoriteButton {\n display: flex;\n\n > :first-child {\n padding: 10px;\n margin: -10px -8px -10px -10px;\n }\n\n .action-counter {\n pointer-events: none;\n user-select: none;\n }\n\n .interactive {\n .svg-inline--fa {\n animation-duration: 0.6s;\n }\n\n &:hover .svg-inline--fa,\n &.-favorited .svg-inline--fa {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n\n .active-marker {\n visibility: visible;\n }\n }\n\n @include focused-style {\n .focus-marker {\n visibility: visible;\n }\n\n .active-marker {\n visibility: hidden;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.ReactButton {\n .reaction-picker-filter {\n padding: 0.5em;\n display: flex;\n\n input {\n flex: 1;\n }\n }\n\n .reaction-picker-divider {\n height: 1px;\n width: 100%;\n margin: 0.5em;\n background-color: var(--border, $fallback--border);\n }\n\n .reaction-picker {\n width: 10em;\n height: 9em;\n font-size: 1.5em;\n overflow-y: scroll;\n display: flex;\n flex-wrap: wrap;\n padding: 0.5em;\n text-align: center;\n align-content: flex-start;\n user-select: none;\n mask:\n linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,\n linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,\n linear-gradient(to top, white, white);\n transition: mask-size 150ms;\n mask-size: 100% 20px, 100% 20px, auto;\n\n /* Autoprefixed seem to ignore this one, and also syntax is different */\n mask-composite: xor;\n mask-composite: exclude;\n\n .emoji-button {\n cursor: pointer;\n flex-basis: 20%;\n line-height: 1.5;\n align-content: center;\n\n &:hover {\n transform: scale(1.25);\n }\n }\n }\n\n .popover-trigger {\n padding: 10px;\n margin: -10px;\n\n &:hover .svg-inline--fa {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n }\n\n @include focused-style {\n .focus-marker {\n visibility: visible;\n }\n }\n }\n}\n\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.RetweetButton {\n display: flex;\n\n > :first-child {\n padding: 10px;\n margin: -10px -8px -10px -10px;\n }\n\n .action-counter {\n pointer-events: none;\n user-select: none;\n }\n\n .interactive {\n .svg-inline--fa {\n animation-duration: 0.6s;\n }\n\n &:hover .svg-inline--fa,\n &.-repeated .svg-inline--fa {\n color: $fallback--cGreen;\n color: var(--cGreen, $fallback--cGreen);\n }\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n\n .active-marker {\n visibility: visible;\n }\n }\n\n @include focused-style {\n .focus-marker {\n visibility: visible;\n }\n\n .active-marker {\n visibility: hidden;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.ExtraButtons {\n .popover-trigger {\n position: static;\n padding: 10px;\n margin: -10px;\n\n &:hover .svg-inline--fa {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n }\n\n .popover-trigger-button {\n /* override of popover internal stuff */\n width: auto;\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n }\n\n @include focused-style {\n .focus-marker {\n visibility: visible;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.avatars {\n display: flex;\n margin: 0;\n padding: 0;\n\n // For hiding overflowing elements\n flex-wrap: wrap;\n height: 24px;\n\n .avatars-item {\n margin: 0 0 5px 5px;\n\n &:first-child {\n padding-left: 5px;\n }\n\n .avatar-small {\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n height: 24px;\n width: 24px;\n }\n }\n}\n","\n@import \"../../variables\";\n\n/* popover styles load on-demand, so we need to override */\n.status-popover.popover {\n font-size: 1rem;\n min-width: 15em;\n max-width: 95%;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n border-style: solid;\n border-width: 1px;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n\n /* TODO cleanup this */\n .Status.Status {\n border: none;\n }\n\n .status-preview-no-content {\n padding: 1em;\n text-align: center;\n\n i {\n font-size: 2em;\n }\n }\n}\n\n","\n@import \"../../variables\";\n\n.user-list-popover {\n padding: 0.5em;\n\n --emoji-size: 16px;\n\n .user-list-row {\n padding: 0.25em;\n display: flex;\n flex-direction: row;\n\n .user-list-names {\n display: flex;\n flex-direction: column;\n margin-left: 0.5em;\n min-width: 5em;\n\n img {\n width: 1em;\n height: 1em;\n }\n }\n\n .user-list-screen-name {\n font-size: 0.65em;\n }\n }\n}\n\n","\n@import \"../../variables\";\n@import \"../../mixins\";\n\n.EmojiReactions {\n display: flex;\n margin-top: 0.25em;\n flex-wrap: wrap;\n\n --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));\n\n .emoji-reaction-container {\n display: flex;\n align-items: stretch;\n margin-top: 0.5em;\n margin-right: 0.5em;\n\n .emoji-reaction-popover {\n padding: 0;\n\n .emoji-reaction-count-button {\n background-color: var(--btn);\n margin: 0;\n height: 100%;\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n box-sizing: border-box;\n min-width: 2em;\n display: inline-flex;\n justify-content: center;\n align-items: center;\n color: $fallback--text;\n color: var(--btnText, $fallback--text);\n\n &.-picked-reaction {\n border: 1px solid var(--accent, $fallback--link);\n margin-right: -1px;\n }\n }\n }\n }\n\n .emoji-reaction {\n padding-left: 0.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n margin: 0;\n\n .reaction-emoji {\n width: var(--emoji-size);\n height: var(--emoji-size);\n margin-right: 0.25em;\n line-height: var(--emoji-size);\n display: flex;\n justify-content: center;\n align-items: center;\n }\n\n .reaction-emoji-content {\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n line-height: inherit;\n overflow: hidden;\n font-size: calc(var(--emoji-size) * 0.8);\n margin: 0;\n }\n\n &:focus {\n outline: none;\n }\n\n .svg-inline--fa {\n color: $fallback--text;\n color: var(--btnText, $fallback--text);\n }\n\n &.-picked-reaction {\n border: 1px solid var(--accent, $fallback--link);\n margin-left: -1px; // offset the border, can't use inset shadows either\n margin-right: -1px;\n\n .svg-inline--fa {\n color: $fallback--link;\n color: var(--accent, $fallback--link);\n }\n }\n\n @include unfocused-style {\n .focus-marker {\n visibility: hidden;\n }\n\n .active-marker {\n visibility: visible;\n }\n }\n\n @include focused-style {\n .svg-inline--fa {\n color: $fallback--link;\n color: var(--accent, $fallback--link);\n }\n\n .focus-marker {\n visibility: visible;\n }\n\n .active-marker {\n visibility: hidden;\n }\n }\n }\n\n .emoji-reaction-expand {\n padding: 0 0.5em;\n margin-right: 0.5em;\n margin-top: 0.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n\n &:hover {\n text-decoration: underline;\n }\n }\n}\n","@import \"../../variables\";\n\n.Status {\n min-width: 0;\n white-space: normal;\n word-wrap: break-word;\n word-break: break-word;\n\n &:hover {\n --_still-image-img-visibility: visible;\n --_still-image-canvas-visibility: hidden;\n --_still-image-label-visibility: hidden;\n }\n\n &.-focused {\n background-color: $fallback--lightBg;\n background-color: var(--selectedPost, $fallback--lightBg);\n color: $fallback--text;\n color: var(--selectedPostText, $fallback--text);\n\n --lightText: var(--selectedPostLightText, $fallback--light);\n --faint: var(--selectedPostFaintText, $fallback--faint);\n --faintLink: var(--selectedPostFaintLink, $fallback--faint);\n --postLink: var(--selectedPostPostLink, $fallback--faint);\n --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);\n --icon: var(--selectedPostIcon, $fallback--icon);\n }\n\n .gravestone {\n padding: var(--status-margin, $status-margin);\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n display: flex;\n\n .deleted-text {\n margin: 0.5em 0;\n align-items: center;\n }\n }\n\n .status-container {\n display: flex;\n padding: var(--status-margin, $status-margin);\n\n > * {\n min-width: 0;\n }\n\n &.-repeat {\n padding-top: 0;\n }\n }\n\n .pin {\n padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n }\n\n ._misclick-prevention & {\n pointer-events: none;\n\n .attachments {\n pointer-events: initial;\n cursor: initial;\n }\n }\n\n .left-side {\n margin-right: var(--status-margin, $status-margin);\n }\n\n .right-side {\n flex: 1;\n min-width: 0;\n }\n\n .usercard {\n margin-bottom: var(--status-margin, $status-margin);\n }\n\n .status-username {\n white-space: nowrap;\n overflow: hidden;\n max-width: 85%;\n font-weight: bold;\n flex-shrink: 1;\n margin-right: 0.4em;\n text-overflow: ellipsis;\n\n --_still_image-label-scale: 0.25;\n --emoji-size: 14px;\n }\n\n .status-favicon {\n height: 18px;\n width: 18px;\n margin-right: 0.4em;\n }\n\n .status-heading {\n margin-bottom: 0.5em;\n }\n\n .heading-name-row {\n display: flex;\n justify-content: space-between;\n line-height: 1.3;\n\n a {\n display: inline-block;\n word-break: break-all;\n }\n }\n\n .account-name {\n min-width: 1.6em;\n margin-right: 0.4em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1 1 0;\n }\n\n .heading-left {\n display: flex;\n min-width: 0;\n }\n\n .heading-right {\n display: flex;\n flex-shrink: 0;\n\n .button-unstyled {\n padding: 5px;\n margin: -5px;\n\n &:hover svg {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n\n .svg-inline--fa {\n margin-left: 0.25em;\n }\n }\n\n .glued-label {\n display: inline-flex;\n white-space: nowrap;\n }\n\n .timeago {\n margin-right: 0.2em;\n }\n\n & .heading-reply-row,\n & .heading-edited-row {\n position: relative;\n align-content: baseline;\n font-size: 0.85em;\n margin-top: 0.2em;\n line-height: 130%;\n max-width: 100%;\n align-items: stretch;\n }\n\n & .reply-to-popover,\n & .reply-to-no-popover,\n & .mentions {\n min-width: 0;\n margin-right: 0.4em;\n flex-shrink: 0;\n }\n\n .reply-glued-label {\n margin-right: 0.5em;\n }\n\n .reply-to-popover {\n .reply-to:hover::before {\n content: \"\";\n display: block;\n position: absolute;\n bottom: 0;\n width: 100%;\n border-bottom: 1px solid var(--faint);\n pointer-events: none;\n }\n\n .faint-link:hover {\n // override default\n text-decoration: none;\n }\n\n &.-strikethrough {\n .reply-to::after {\n content: \"\";\n display: block;\n position: absolute;\n top: 50%;\n width: 100%;\n border-bottom: 1px solid var(--faint);\n pointer-events: none;\n }\n }\n }\n\n & .mentions,\n & .reply-to {\n white-space: nowrap;\n position: relative;\n }\n\n & .mentions-text,\n & .reply-to-text {\n color: var(--faint);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .mentions-line {\n display: inline;\n }\n\n .replies {\n margin-top: 0.25em;\n line-height: 1.3;\n font-size: 0.85em;\n display: flex;\n flex-wrap: wrap;\n\n & > * {\n margin-right: 0.4em;\n }\n }\n\n .reply-link {\n height: 17px;\n }\n\n .repeat-info {\n padding: 0.4em var(--status-margin, $status-margin);\n\n .repeat-icon {\n color: $fallback--cGreen;\n color: var(--cGreen, $fallback--cGreen);\n }\n }\n\n .repeater-avatar {\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n margin-left: 28px;\n width: 20px;\n height: 20px;\n }\n\n .repeater-name {\n text-overflow: ellipsis;\n margin-right: 0;\n\n .emoji {\n width: 14px;\n height: 14px;\n vertical-align: middle;\n object-fit: contain;\n }\n }\n\n .status-fadein {\n animation-duration: 0.4s;\n animation-name: fadein;\n }\n\n @keyframes fadein {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n }\n\n .status-actions {\n position: relative;\n width: 100%;\n display: flex;\n margin-top: var(--status-margin, $status-margin);\n\n > * {\n max-width: 4em;\n flex: 1;\n }\n }\n\n .muted {\n padding: 0.25em 0.6em;\n height: 1.2em;\n line-height: 1.2em;\n text-overflow: ellipsis;\n overflow: hidden;\n display: flex;\n flex-wrap: nowrap;\n\n & .status-username,\n & .mute-thread,\n & .mute-words {\n word-wrap: normal;\n word-break: normal;\n white-space: nowrap;\n }\n\n & .status-username,\n & .mute-words {\n text-overflow: ellipsis;\n overflow: hidden;\n }\n\n .status-username {\n font-weight: normal;\n flex: 0 1 auto;\n margin-right: 0.2em;\n font-size: smaller;\n }\n\n .mute-thread {\n flex: 0 0 auto;\n }\n\n .mute-words {\n flex: 1 0 5em;\n margin-left: 0.2em;\n\n &::before {\n content: \" \";\n }\n }\n\n .unmute {\n flex: 0 0 auto;\n margin-left: auto;\n display: block;\n }\n }\n\n .reply-form {\n padding-top: 0;\n padding-bottom: 0;\n }\n\n .reply-body {\n flex: 1;\n }\n\n .favs-repeated-users {\n margin-top: var(--status-margin, $status-margin);\n }\n\n .stats {\n width: 100%;\n display: flex;\n line-height: 1em;\n }\n\n .avatar-row {\n flex: 1;\n overflow: hidden;\n position: relative;\n display: flex;\n align-items: center;\n\n &::before {\n content: \"\";\n position: absolute;\n height: 100%;\n width: 1px;\n left: 0;\n background-color: var(--faint, $fallback--faint);\n }\n }\n\n .stat-count {\n margin-right: var(--status-margin, $status-margin);\n user-select: none;\n\n .stat-title {\n color: var(--faint, $fallback--faint);\n font-size: 0.85em;\n text-transform: uppercase;\n position: relative;\n }\n\n .stat-number {\n font-weight: bolder;\n font-size: 1.1em;\n line-height: 1em;\n }\n\n &:hover .stat-title {\n text-decoration: underline;\n }\n }\n\n @media all and (max-width: 800px) {\n .repeater-avatar {\n margin-left: 20px;\n }\n\n .post-avatar {\n width: 40px;\n height: 40px;\n\n // TODO define those other way somehow?\n // stylelint-disable rscss/class-format\n &.-compact {\n width: 32px;\n height: 32px;\n }\n }\n }\n\n .quoted-status {\n margin-top: 0.5em;\n border: 1px solid var(--border, $fallback--border);\n border-radius: var(--attachmentRadius, $fallback--attachmentRadius);\n\n &.-unavailable-prompt {\n padding: 0.5em;\n }\n }\n\n .display-quoted-status-button {\n margin: 0.5em;\n\n &-icon {\n color: inherit;\n }\n }\n}\n","@import \"../../variables\";\n\n.Report {\n .report-content {\n margin: 0.5em 0 1em;\n }\n\n .report-state {\n margin: 0.5em 0 1em;\n }\n\n .reported-status {\n border: 1px solid $fallback--faint;\n border-color: var(--faint, $fallback--faint);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n color: $fallback--text;\n color: var(--text, $fallback--text);\n display: block;\n padding: 0.5em;\n margin: 0.5em 0;\n\n .status-content {\n pointer-events: none;\n }\n\n .reported-status-heading {\n display: flex;\n width: 100%;\n justify-content: space-between;\n margin-bottom: 0.2em;\n }\n\n .reported-status-name {\n font-weight: bold;\n }\n }\n\n .note {\n width: 100%;\n margin-bottom: 0.5em;\n }\n}\n","@import \"../../variables\";\n\n// TODO Copypaste from Status, should unify it somehow\n.Notification {\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n word-wrap: break-word;\n word-break: break-word;\n\n --emoji-size: 14px;\n\n &:hover {\n --_still-image-img-visibility: visible;\n --_still-image-canvas-visibility: hidden;\n --_still-image-label-visibility: hidden;\n }\n\n &.-muted {\n padding: 0.25em 0.6em;\n height: 1.2em;\n line-height: 1.2em;\n text-overflow: ellipsis;\n overflow: hidden;\n display: flex;\n flex-wrap: nowrap;\n\n & .status-username,\n & .mute-thread,\n & .mute-words {\n word-wrap: normal;\n word-break: normal;\n white-space: nowrap;\n }\n\n & .status-username,\n & .mute-words {\n text-overflow: ellipsis;\n overflow: hidden;\n }\n\n .status-username {\n font-weight: normal;\n flex: 0 1 auto;\n margin-right: 0.2em;\n font-size: smaller;\n }\n\n .mute-thread {\n flex: 0 0 auto;\n }\n\n .mute-words {\n flex: 1 0 5em;\n margin-left: 0.2em;\n\n &::before {\n content: \" \";\n }\n }\n\n .unmute {\n flex: 0 0 auto;\n margin-left: auto;\n display: block;\n }\n }\n\n .type-icon {\n margin: 0 0.1em;\n }\n\n &.-type--repeat .type-icon {\n color: $fallback--cGreen;\n color: var(--cGreen, $fallback--cGreen);\n }\n\n &.-type--follow .type-icon {\n color: $fallback--cBlue;\n color: var(--cBlue, $fallback--cBlue);\n }\n\n &.-type--follow-request .type-icon {\n color: $fallback--cBlue;\n color: var(--cBlue, $fallback--cBlue);\n }\n\n &.-type--like .type-icon {\n color: orange;\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n &.-type--move .type-icon {\n color: $fallback--cBlue;\n color: var(--cBlue, $fallback--cBlue);\n }\n}\n","@import \"../../variables\";\n\n.Notifications {\n &:not(.minimal) {\n // a bit of a hack to allow scrolling below notifications\n padding-bottom: 15em;\n }\n\n .loadmore-error {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n\n .notification {\n position: relative;\n\n .notification-overlay {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n &.unseen {\n .notification-overlay {\n background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px);\n }\n }\n }\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.notification {\n box-sizing: border-box;\n\n &:hover .animated.Avatar {\n canvas {\n display: none;\n }\n\n img {\n visibility: visible;\n }\n }\n\n &:last-child .Notification {\n border-bottom: none;\n }\n\n .non-mention {\n display: flex;\n flex: 1;\n flex-wrap: nowrap;\n padding: 0.6em;\n min-width: 0;\n\n .avatar-container {\n width: 32px;\n height: 32px;\n }\n\n .faint {\n --link: var(--faintLink);\n --text: var(--faint);\n }\n }\n\n .follow-request-accept {\n &:hover {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n }\n\n .follow-request-reject {\n &:hover {\n color: $fallback--cRed;\n color: var(--cRed, $fallback--cRed);\n }\n }\n\n .follow-text,\n .move-text {\n padding: 0.5em 0;\n overflow-wrap: break-word;\n display: flex;\n justify-content: space-between;\n\n .follow-name {\n display: block;\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n }\n\n /* TODO cleanup this */\n .Status {\n flex: 1;\n }\n\n time {\n white-space: nowrap;\n }\n\n .notification-right {\n flex: 1;\n padding-left: 0.8em;\n min-width: 0;\n\n .timeago {\n min-width: 3em;\n text-align: right;\n }\n\n .timeago-link {\n margin-right: 0.2em;\n }\n\n .expand-icon {\n .svg-inline--fa {\n margin-left: 0.25em;\n }\n }\n }\n\n .emoji-reaction-emoji {\n font-size: 1.3em;\n max-width: 1.25em;\n height: 1.25em;\n width: auto;\n }\n\n .emoji-reaction-emoji-image {\n vertical-align: middle;\n object-fit: contain;\n }\n\n .notification-details {\n min-width: 0;\n word-wrap: break-word;\n line-height: var(--post-line-height);\n position: relative;\n overflow: hidden;\n width: 100%;\n flex: 1 1 0;\n display: flex;\n flex-wrap: nowrap;\n justify-content: space-between;\n\n .name-and-action {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .username {\n font-weight: bolder;\n max-width: 100%;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .timeago {\n margin-right: 0.2em;\n }\n\n .status-content {\n margin: 0;\n max-height: 300px;\n }\n\n h1 {\n word-break: break-all;\n margin: 0 0 0.3em;\n padding: 0;\n font-size: 1em;\n line-height: 1.5;\n\n small {\n font-weight: lighter;\n }\n }\n\n p {\n margin: 0;\n margin-top: 0;\n margin-bottom: 0.3em;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.MobileNav {\n z-index: var(--ZI_navbar);\n\n .mobile-nav {\n display: grid;\n line-height: var(--navbar-height);\n grid-template-rows: 50px;\n grid-template-columns: 2fr auto;\n width: 100%;\n box-sizing: border-box;\n\n a {\n color: var(--topBarLink, $fallback--link);\n }\n }\n\n .mobile-inner-nav {\n width: 100%;\n display: flex;\n align-items: center;\n }\n\n .mobile-nav-button {\n display: inline-block;\n text-align: center;\n padding: 0 1em;\n position: relative;\n cursor: pointer;\n }\n\n .site-name {\n padding: 0 0.3em;\n display: inline-block;\n }\n\n .item {\n /* moslty just to get rid of extra whitespaces */\n display: flex;\n }\n\n .alert-dot {\n border-radius: 100%;\n height: 8px;\n width: 8px;\n position: absolute;\n left: calc(50% - 4px);\n top: calc(50% - 4px);\n margin-left: 6px;\n margin-top: -6px;\n background-color: $fallback--cRed;\n background-color: var(--badgeNotification, $fallback--cRed);\n }\n\n .mobile-notifications-drawer {\n width: 100%;\n height: 100vh;\n overflow-x: hidden;\n position: fixed;\n top: 0;\n left: 0;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n transition-property: transform;\n transition-duration: 0.25s;\n transform: translateX(0);\n z-index: var(--ZI_navbar);\n -webkit-overflow-scrolling: touch;\n\n &.-closed {\n transform: translateX(100%);\n box-shadow: none;\n }\n }\n\n .mobile-notifications-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: calc(var(--ZI_navbar) + 100);\n width: 100%;\n height: 50px;\n line-height: 50px;\n position: absolute;\n color: var(--topBarText);\n background-color: $fallback--fg;\n background-color: var(--topBar, $fallback--fg);\n box-shadow: 0 0 4px rgb(0 0 0 / 60%);\n box-shadow: var(--topBarShadow);\n\n .spacer {\n flex: 1;\n }\n\n .title {\n font-size: 1.3em;\n margin-left: 0.6em;\n }\n }\n\n .pins {\n flex: 1;\n\n .pinned-item {\n flex-grow: 1;\n }\n }\n\n .mobile-notifications {\n margin-top: 50px;\n width: 100vw;\n height: calc(100vh - var(--navbar-height));\n overflow-x: hidden;\n overflow-y: scroll;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n\n .notifications {\n padding: 0;\n border-radius: 0;\n box-shadow: none;\n\n .panel {\n border-radius: 0;\n margin: 0;\n box-shadow: none;\n }\n\n .panel::after {\n border-radius: 0;\n }\n\n .panel .panel-heading {\n border-radius: 0;\n box-shadow: none;\n }\n }\n }\n\n .confirm-modal.dark-overlay {\n &::before {\n z-index: 3000;\n }\n\n .dialog-modal.panel {\n z-index: 3001;\n }\n }\n}\n\n","\n@import \"../../variables\";\n\n.SearchBar {\n display: inline-flex;\n align-items: baseline;\n vertical-align: baseline;\n justify-content: flex-end;\n\n &.-expanded {\n width: 100%;\n }\n\n .search-bar-input,\n .search-button {\n height: 29px;\n }\n\n .search-bar-input {\n flex: 1 0 auto;\n }\n\n .cancel-search {\n height: 50px;\n }\n\n .cancel-icon {\n color: $fallback--text;\n color: var(--btnTopBarText, $fallback--text);\n }\n}\n\n","@import \"../../variables\";\n\n.DesktopNav {\n width: 100%;\n z-index: var(--ZI_navbar);\n\n input {\n color: var(--inputTopbarText, var(--inputText));\n }\n\n a {\n color: var(--topBarLink, $fallback--link);\n }\n\n .inner-nav {\n display: grid;\n grid-template-rows: var(--navbar-height);\n grid-template-columns: 2fr auto 2fr;\n grid-template-areas: \"sitename logo actions\";\n box-sizing: border-box;\n padding: 0 1.2em;\n margin: auto;\n max-width: 980px;\n }\n\n &.-column-stretch .inner-nav {\n --miniColumn: 25rem;\n --maxiColumn: 45rem;\n --columnGap: 1em;\n\n max-width:\n calc(\n var(--sidebarColumnWidth, var(--miniColumn)) +\n var(--contentColumnWidth, var(--maxiColumn)) +\n var(--columnGap)\n );\n }\n\n &.-logoLeft .inner-nav {\n grid-template-columns: auto 2fr 2fr;\n grid-template-areas: \"logo sitename actions\";\n }\n\n &.-column-stretch.-wide .inner-nav {\n max-width:\n calc(\n var(--sidebarColumnWidth, var(--miniColumn)) +\n var(--contentColumnWidth, var(--maxiColumn)) +\n var(--notifsColumnWidth, var(--miniColumn)) +\n var(--columnGap)\n );\n }\n\n .button-default {\n &,\n svg {\n color: $fallback--text;\n color: var(--btnTopBarText, $fallback--text);\n }\n\n &:active {\n background-color: $fallback--fg;\n background-color: var(--btnPressedTopBar, $fallback--fg);\n color: $fallback--text;\n color: var(--btnPressedTopBarText, $fallback--text);\n }\n\n &:disabled {\n color: $fallback--text;\n color: var(--btnDisabledTopBarText, $fallback--text);\n }\n\n &.toggled {\n color: $fallback--text;\n color: var(--btnToggledTopBarText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--btnToggledTopBar, $fallback--fg);\n }\n }\n\n .logo {\n grid-area: logo;\n position: relative;\n transition: opacity;\n transition-timing-function: ease-out;\n transition-duration: 100ms;\n\n @media all and (min-width: 800px) {\n /* stylelint-disable-next-line declaration-no-important */\n opacity: 1 !important;\n }\n\n .mask {\n mask-repeat: no-repeat;\n mask-position: center;\n mask-size: contain;\n background-color: $fallback--fg;\n background-color: var(--topBarText, $fallback--fg);\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n }\n\n img {\n display: inline-block;\n height: var(--navbar-height);\n }\n }\n\n .nav-icon {\n margin-left: 0.2em;\n width: 2em;\n height: 100%;\n text-align: center;\n\n .svg-inline--fa {\n color: $fallback--link;\n color: var(--topBarLink, $fallback--link);\n }\n }\n\n .sitename {\n grid-area: sitename;\n }\n\n .actions {\n grid-area: actions;\n }\n\n .item {\n flex: 1;\n line-height: var(--navbar-height);\n height: var(--navbar-height);\n overflow: hidden;\n display: flex;\n flex-wrap: wrap;\n\n &.right {\n justify-content: flex-end;\n text-align: right;\n }\n }\n\n .spacer {\n width: 1em;\n }\n}\n","\n@import \"../../variables\";\n\n.list {\n &-item:not(:last-child) {\n border-bottom: 1px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n\n &-empty-content {\n text-align: center;\n padding: 10px;\n }\n}\n","\n@import \"../../variables\";\n\n.user-reporting-panel {\n width: 90vw;\n max-width: 700px;\n min-height: 20vh;\n max-height: 80vh;\n\n .panel-body {\n display: flex;\n flex-direction: column-reverse;\n border-top: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n overflow: hidden;\n }\n\n &-left {\n padding: 1.1em 0.7em 0.7em;\n line-height: var(--post-line-height);\n box-sizing: border-box;\n\n > div {\n margin-bottom: 1em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n p {\n margin-top: 0;\n }\n\n textarea.form-control {\n line-height: 16px;\n resize: none;\n overflow: hidden;\n transition: min-height 200ms 100ms;\n min-height: 44px;\n width: 100%;\n }\n\n .btn {\n min-width: 10em;\n padding: 0 2em;\n }\n\n .alert {\n margin: 1em 0 0;\n line-height: 1.3em;\n }\n }\n\n &-right {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n }\n\n &-sitem {\n display: flex;\n justify-content: space-between;\n\n /* TODO cleanup this */\n > .Status {\n flex: 1;\n }\n\n > .checkbox {\n margin: 0.75em;\n }\n }\n\n @media all and (min-width: 801px) {\n .panel-body {\n flex-direction: row;\n }\n\n &-left {\n width: 50%;\n max-width: 320px;\n border-right: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n padding: 1.1em;\n\n > div {\n margin-bottom: 2em;\n }\n }\n\n &-right {\n width: 50%;\n flex: 1 1 auto;\n margin-bottom: 12px;\n }\n }\n}\n","\n.modal-view.edit-form-modal-view {\n align-items: flex-start;\n}\n\n.edit-form-modal-panel {\n flex-shrink: 0;\n margin-top: 25%;\n margin-bottom: 2em;\n width: 100%;\n max-width: 700px;\n\n @media (orientation: landscape) {\n margin-top: 8%;\n }\n\n .form-bottom-left {\n max-width: 6.5em;\n\n .emoji-icon {\n justify-content: right;\n }\n }\n}\n","\n.modal-view.post-form-modal-view {\n align-items: flex-start;\n}\n\n.post-form-modal-panel {\n flex-shrink: 0;\n margin-top: 25%;\n margin-bottom: 2em;\n width: 100%;\n max-width: 700px;\n\n @media (orientation: landscape) {\n margin-top: 8%;\n }\n}\n","\n.modal-view.status-history-modal-view {\n align-items: flex-start;\n}\n\n.status-history-modal-panel {\n flex-shrink: 0;\n margin-top: 25%;\n margin-bottom: 2em;\n width: 100%;\n max-width: 700px;\n\n @media (orientation: landscape) {\n margin-top: 8%;\n }\n}\n","\n@import \"../../variables\";\n\n.global-notice-list {\n position: fixed;\n top: calc(var(--navbar-height) + 0.5em);\n width: 100%;\n pointer-events: none;\n z-index: var(--ZI_navbar_popovers);\n display: flex;\n flex-direction: column;\n align-items: center;\n\n .global-notice {\n pointer-events: auto;\n text-align: center;\n width: 40em;\n max-width: calc(100% - 3em);\n display: flex;\n padding-left: 1.5em;\n line-height: 2;\n margin-bottom: 0.5em;\n\n .notice-message {\n flex: 1 1 100%;\n }\n }\n\n .global-error {\n background-color: var(--alertPopupError, $fallback--cRed);\n color: var(--alertPopupErrorText, $fallback--text);\n\n .svg-inline--fa {\n color: var(--alertPopupErrorText, $fallback--text);\n }\n }\n\n .global-warning {\n background-color: var(--alertPopupWarning, $fallback--cOrange);\n color: var(--alertPopupWarningText, $fallback--text);\n\n .svg-inline--fa {\n color: var(--alertPopupWarningText, $fallback--text);\n }\n }\n\n .global-success {\n background-color: var(--alertPopupSuccess, $fallback--cGreen);\n color: var(--alertPopupSuccessText, $fallback--text);\n\n .svg-inline--fa {\n color: var(--alertPopupSuccessText, $fallback--text);\n }\n }\n\n .global-info {\n background-color: var(--alertPopupNeutral, $fallback--fg);\n color: var(--alertPopupNeutralText, $fallback--text);\n\n .svg-inline--fa {\n color: var(--alertPopupNeutralText, $fallback--text);\n }\n }\n\n .close-notice {\n padding-right: 0.2em;\n\n .svg-inline--fa:hover {\n opacity: 0.6;\n }\n }\n}\n","// stylelint-disable rscss/class-format\n/* stylelint-disable no-descending-specificity */\n@import \"./variables\";\n@import \"./panel\";\n\n:root {\n --navbar-height: 3.5rem;\n --post-line-height: 1.4;\n // Z-Index stuff\n --ZI_media_modal: 9000;\n --ZI_modals_popovers: 8500;\n --ZI_modals: 8000;\n --ZI_navbar_popovers: 7500;\n --ZI_navbar: 7000;\n --ZI_popovers: 6000;\n}\n\nhtml {\n font-size: 14px;\n // overflow-x: clip causes my browser's tab to crash with SIGILL lul\n}\n\nbody {\n font-family: sans-serif;\n font-family: var(--interfaceFont, sans-serif);\n margin: 0;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overscroll-behavior-y: none;\n overflow-x: clip;\n overflow-y: scroll;\n\n &.hidden {\n display: none;\n }\n}\n\n// ## Custom scrollbars\n// Only show custom scrollbars on devices which\n// have a cursor/pointer to operate them\n@media (any-pointer: fine) {\n * {\n scrollbar-color: var(--btn) transparent;\n\n &::-webkit-scrollbar {\n background: transparent;\n }\n\n &::-webkit-scrollbar-button,\n &::-webkit-scrollbar-thumb {\n background-color: var(--btn);\n box-shadow: var(--buttonShadow);\n border-radius: var(--btnRadius);\n }\n\n // horizontal/vertical/increment/decrement are webkit-specific stuff\n // that indicates whether we're affecting vertical scrollbar, increase button etc\n // stylelint-disable selector-pseudo-class-no-unknown\n &::-webkit-scrollbar-button {\n --___bgPadding: 2px;\n\n color: var(--btnText);\n background-repeat: no-repeat, no-repeat;\n\n &:horizontal {\n background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));\n\n &:increment {\n background-image:\n linear-gradient(45deg, var(--btnText) 50%, transparent 51%),\n linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);\n background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);\n }\n\n &:decrement {\n background-image:\n linear-gradient(45deg, transparent 50%, var(--btnText) 51%),\n linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);\n background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);\n }\n }\n\n &:vertical {\n background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;\n\n &:increment {\n background-image:\n linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),\n linear-gradient(45deg, transparent 50%, var(--btnText) 51%);\n background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;\n }\n\n &:decrement {\n background-image:\n linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),\n linear-gradient(45deg, var(--btnText) 50%, transparent 51%);\n background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;\n }\n }\n }\n // stylelint-enable selector-pseudo-class-no-unknown\n }\n // Body should have background to scrollbar otherwise it will use white (body color?)\n html {\n scrollbar-color: var(--selectedMenu) var(--wallpaper);\n background: var(--wallpaper);\n }\n}\n\na {\n text-decoration: none;\n color: $fallback--link;\n color: var(--link, $fallback--link);\n}\n\nh4 {\n margin: 0;\n}\n\n.iconLetter {\n display: inline-block;\n text-align: center;\n font-weight: 1000;\n}\n\ni[class*=\"icon-\"],\n.svg-inline--fa,\n.iconLetter {\n color: $fallback--icon;\n color: var(--icon, $fallback--icon);\n}\n\n.button-unstyled:hover,\na:hover {\n > i[class*=\"icon-\"],\n > .svg-inline--fa,\n > .iconLetter {\n color: var(--text);\n }\n}\n\nnav {\n z-index: var(--ZI_navbar);\n background-color: $fallback--fg;\n background-color: var(--topBar, $fallback--fg);\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n box-shadow: 0 0 4px rgb(0 0 0 / 60%);\n box-shadow: var(--topBarShadow);\n box-sizing: border-box;\n height: var(--navbar-height);\n position: fixed;\n}\n\n#sidebar {\n grid-area: sidebar;\n}\n\n#modal {\n position: absolute;\n z-index: var(--ZI_modals);\n}\n\n.column.-scrollable {\n top: var(--navbar-height);\n position: sticky;\n}\n\n#main-scroller {\n grid-area: content;\n position: relative;\n}\n\n#notifs-column {\n grid-area: notifs;\n}\n\n.app-bg-wrapper {\n position: fixed;\n height: 100%;\n top: var(--navbar-height);\n z-index: -1000;\n left: 0;\n right: -20px;\n background-size: cover;\n background-repeat: no-repeat;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-position: 50%;\n}\n\n.underlay {\n grid-column: 1 / span 3;\n grid-row: 1 / 1;\n pointer-events: none;\n background-color: rgb(0 0 0 / 15%);\n background-color: var(--underlay, rgb(0 0 0 / 15%));\n z-index: -1000;\n}\n\n.app-layout {\n --miniColumn: 25rem;\n --maxiColumn: 45rem;\n --columnGap: 1em;\n --status-margin: 0.75em;\n --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));\n --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));\n --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));\n\n position: relative;\n display: grid;\n grid-template-columns:\n var(--effectiveSidebarColumnWidth)\n var(--effectiveContentColumnWidth);\n grid-template-areas: \"sidebar content\";\n grid-template-rows: 1fr;\n box-sizing: border-box;\n margin: 0 auto;\n align-content: flex-start;\n flex-wrap: wrap;\n justify-content: center;\n min-height: 100vh;\n overflow-x: clip;\n\n .column {\n --___columnMargin: var(--columnGap);\n\n display: grid;\n grid-template-columns: 100%;\n box-sizing: border-box;\n grid-row: 1 / 1;\n margin: 0 calc(var(--___columnMargin) / 2);\n padding: calc(var(--___columnMargin)) 0;\n row-gap: var(--___columnMargin);\n align-content: start;\n\n &:not(.-scrollable) {\n margin-top: var(--navbar-height);\n }\n\n &:hover {\n z-index: 2;\n }\n\n &.-full-height {\n margin-bottom: 0;\n padding-top: 0;\n padding-bottom: 0;\n }\n\n &.-scrollable {\n --___paddingIncrease: calc(var(--columnGap) / 2);\n\n position: sticky;\n top: var(--navbar-height);\n max-height: calc(100vh - var(--navbar-height));\n overflow-y: auto;\n overflow-x: hidden;\n margin-left: calc(var(--___paddingIncrease) * -1);\n padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);\n\n // On browsers that don't support hiding scrollbars we enforce \"show scrolbars\" mode\n // might implement old style of hiding scrollbars later if there's demand\n @supports (scrollbar-width: none) or (-webkit-text-fill-color: initial) {\n &:not(.-show-scrollbar) {\n scrollbar-width: none;\n margin-right: calc(var(--___paddingIncrease) * -1);\n padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);\n\n &::-webkit-scrollbar {\n display: block;\n width: 0;\n }\n }\n }\n\n .panel-heading.-sticky {\n top: calc(var(--columnGap) / -1);\n }\n }\n }\n\n &.-has-new-post-button {\n .column {\n padding-bottom: 10rem;\n }\n }\n\n &.-no-sticky-headers {\n .column {\n .panel-heading.-sticky {\n position: relative;\n top: 0;\n }\n }\n }\n\n .column-inner {\n display: grid;\n grid-template-columns: 100%;\n box-sizing: border-box;\n row-gap: 1em;\n align-content: start;\n }\n\n &.-reverse:not(.-wide, .-mobile) {\n grid-template-columns:\n var(--effectiveContentColumnWidth)\n var(--effectiveSidebarColumnWidth);\n grid-template-areas: \"content sidebar\";\n }\n\n &.-wide {\n grid-template-columns:\n var(--effectiveSidebarColumnWidth)\n var(--effectiveContentColumnWidth)\n var(--effectiveNotifsColumnWidth);\n grid-template-areas: \"sidebar content notifs\";\n\n &.-reverse {\n grid-template-columns:\n var(--effectiveNotifsColumnWidth)\n var(--effectiveContentColumnWidth)\n var(--effectiveSidebarColumnWidth);\n grid-template-areas: \"notifs content sidebar\";\n }\n }\n\n &.-mobile {\n grid-template-columns: 100vw;\n grid-template-areas: \"content\";\n padding: 0;\n\n .column {\n padding-top: 0;\n margin: var(--navbar-height) 0 0 0;\n }\n\n .panel-heading,\n .panel-heading::after,\n .panel-heading::before,\n .panel,\n .panel::after {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n\n #sidebar,\n #notifs-column {\n display: none;\n }\n }\n\n &.-normal {\n #notifs-column {\n display: none;\n }\n }\n}\n\n.text-center {\n text-align: center;\n}\n\n.button-default {\n user-select: none;\n color: $fallback--text;\n color: var(--btnText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--btn, $fallback--fg);\n border: none;\n border-radius: $fallback--btnRadius;\n border-radius: var(--btnRadius, $fallback--btnRadius);\n cursor: pointer;\n box-shadow: $fallback--buttonShadow;\n box-shadow: var(--buttonShadow);\n font-size: 1em;\n font-family: sans-serif;\n font-family: var(--interfaceFont, sans-serif);\n\n &.-sublime {\n background: transparent;\n }\n\n i[class*=\"icon-\"],\n .svg-inline--fa {\n color: $fallback--text;\n color: var(--btnText, $fallback--text);\n }\n\n &::-moz-focus-inner {\n border: none;\n }\n\n &:hover {\n box-shadow: 0 0 4px rgb(255 255 255 / 30%);\n box-shadow: var(--buttonHoverShadow);\n }\n\n &:active {\n box-shadow:\n 0 0 4px 0 rgb(255 255 255 / 30%),\n 0 1px 0 0 rgb(0 0 0 / 20%) inset,\n 0 -1px 0 0 rgb(255 255 255 / 20%) inset;\n box-shadow: var(--buttonPressedShadow);\n color: $fallback--text;\n color: var(--btnPressedText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--btnPressed, $fallback--fg);\n\n svg,\n i {\n color: $fallback--text;\n color: var(--btnPressedText, $fallback--text);\n }\n }\n\n &:disabled {\n cursor: not-allowed;\n color: $fallback--text;\n color: var(--btnDisabledText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--btnDisabled, $fallback--fg);\n\n svg,\n i {\n color: $fallback--text;\n color: var(--btnDisabledText, $fallback--text);\n }\n }\n\n &.toggled {\n color: $fallback--text;\n color: var(--btnToggledText, $fallback--text);\n background-color: $fallback--fg;\n background-color: var(--btnToggled, $fallback--fg);\n box-shadow:\n 0 0 4px 0 rgb(255 255 255 / 30%),\n 0 1px 0 0 rgb(0 0 0 / 20%) inset,\n 0 -1px 0 0 rgb(255 255 255 / 20%) inset;\n box-shadow: var(--buttonPressedShadow);\n\n svg,\n i {\n color: $fallback--text;\n color: var(--btnToggledText, $fallback--text);\n }\n }\n\n &.danger {\n // TODO: add better color variable\n color: $fallback--text;\n color: var(--alertErrorPanelText, $fallback--text);\n background-color: $fallback--alertError;\n background-color: var(--alertError, $fallback--alertError);\n }\n}\n\n.button-unstyled {\n background: none;\n border: none;\n outline: none;\n display: inline;\n text-align: initial;\n font-size: 100%;\n font-family: inherit;\n padding: 0;\n line-height: unset;\n cursor: pointer;\n box-sizing: content-box;\n color: inherit;\n\n &.-link {\n color: $fallback--link;\n color: var(--link, $fallback--link);\n }\n\n &.-fullwidth {\n width: 100%;\n }\n\n &.-hover-highlight {\n &:hover svg {\n color: $fallback--lightText;\n color: var(--lightText, $fallback--lightText);\n }\n }\n}\n\ninput,\ntextarea,\n.input {\n &.unstyled {\n border-radius: 0;\n background: none;\n box-shadow: none;\n height: unset;\n }\n\n --_padding: 0.5em;\n\n border: none;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n box-shadow:\n 0 1px 0 0 rgb(0 0 0 / 20%) inset,\n 0 -1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 0 2px 0 rgb(0 0 0 / 100%) inset;\n box-shadow: var(--inputShadow);\n background-color: $fallback--fg;\n background-color: var(--input, $fallback--fg);\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n font-family: sans-serif;\n font-family: var(--inputFont, sans-serif);\n font-size: 1em;\n margin: 0;\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n line-height: 2;\n hyphens: none;\n padding: 0 var(--_padding);\n\n &:disabled,\n &[disabled=\"disabled\"],\n &.disabled {\n cursor: not-allowed;\n opacity: 0.5;\n }\n\n &[type=\"range\"] {\n background: none;\n border: none;\n margin: 0;\n box-shadow: none;\n flex: 1;\n }\n\n &[type=\"radio\"] {\n display: none;\n\n &:checked + label::before {\n box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;\n box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;\n background-color: var(--accent, $fallback--link);\n }\n\n &:disabled {\n &,\n & + label,\n & + label::before {\n opacity: 0.5;\n }\n }\n\n + label::before {\n flex-shrink: 0;\n display: inline-block;\n content: \"\";\n transition: box-shadow 200ms;\n width: 1.1em;\n height: 1.1em;\n border-radius: 100%; // Radio buttons should always be circle\n box-shadow: 0 0 2px black inset;\n box-shadow: var(--inputShadow);\n margin-right: 0.5em;\n background-color: $fallback--fg;\n background-color: var(--input, $fallback--fg);\n vertical-align: top;\n text-align: center;\n line-height: 1.1;\n font-size: 1.1em;\n box-sizing: border-box;\n color: transparent;\n overflow: hidden;\n }\n }\n\n &[type=\"checkbox\"] {\n &:checked + label::before {\n color: $fallback--text;\n color: var(--inputText, $fallback--text);\n }\n\n &:disabled {\n &,\n & + label,\n & + label::before {\n opacity: 0.5;\n }\n }\n\n + label::before {\n flex-shrink: 0;\n display: inline-block;\n content: \"✓\";\n transition: color 200ms;\n width: 1.1em;\n height: 1.1em;\n border-radius: $fallback--checkboxRadius;\n border-radius: var(--checkboxRadius, $fallback--checkboxRadius);\n box-shadow: 0 0 2px black inset;\n box-shadow: var(--inputShadow);\n margin-right: 0.5em;\n background-color: $fallback--fg;\n background-color: var(--input, $fallback--fg);\n vertical-align: top;\n text-align: center;\n line-height: 1.1;\n font-size: 1.1em;\n box-sizing: border-box;\n color: transparent;\n overflow: hidden;\n }\n }\n\n &.resize-height {\n resize: vertical;\n }\n}\n\n// Textareas should have stock line-height + vertical padding instead of huge line-height\ntextarea {\n padding: var(--_padding);\n line-height: var(--post-line-height);\n}\n\noption {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n}\n\n.hide-number-spinner {\n appearance: textfield;\n\n &[type=\"number\"]::-webkit-inner-spin-button,\n &[type=\"number\"]::-webkit-outer-spin-button {\n opacity: 0;\n display: none;\n }\n}\n\n.cards-list {\n list-style: none;\n display: grid;\n grid-auto-flow: row dense;\n grid-template-columns: 1fr 1fr;\n\n li {\n border: 1px solid var(--border);\n border-radius: var(--inputRadius);\n padding: 0.5em;\n margin: 0.25em;\n }\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-group {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n\n button,\n .button-dropdown {\n position: relative;\n flex: 1 1 auto;\n\n &:not(:last-child),\n &:not(:last-child) .button-default {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n &:not(:first-child),\n &:not(:first-child) .button-default {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n\n.fa {\n color: grey;\n}\n\n.mobile-shown {\n display: none;\n}\n\n.badge {\n box-sizing: border-box;\n display: inline-block;\n border-radius: 99px;\n max-width: 10em;\n min-width: 1.7em;\n height: 1.3em;\n padding: 0.15em;\n vertical-align: middle;\n font-weight: normal;\n font-style: normal;\n font-size: 0.9em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n\n &.badge-notification {\n background-color: $fallback--cRed;\n background-color: var(--badgeNotification, $fallback--cRed);\n color: white;\n color: var(--badgeNotificationText, white);\n }\n}\n\n.alert {\n margin: 0 0.35em;\n padding: 0 0.25em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n\n &.error {\n background-color: $fallback--alertError;\n background-color: var(--alertError, $fallback--alertError);\n color: $fallback--text;\n color: var(--alertErrorText, $fallback--text);\n\n .panel-heading & {\n color: $fallback--text;\n color: var(--alertErrorPanelText, $fallback--text);\n }\n }\n\n &.warning {\n background-color: $fallback--alertWarning;\n background-color: var(--alertWarning, $fallback--alertWarning);\n color: $fallback--text;\n color: var(--alertWarningText, $fallback--text);\n\n .panel-heading & {\n color: $fallback--text;\n color: var(--alertWarningPanelText, $fallback--text);\n }\n }\n\n &.success {\n background-color: var(--alertSuccess, $fallback--alertWarning);\n color: var(--alertSuccessText, $fallback--text);\n\n .panel-heading & {\n color: var(--alertSuccessPanelText, $fallback--text);\n }\n }\n}\n\n.faint {\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n}\n\n.faint-link {\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n\n &:hover {\n text-decoration: underline;\n }\n}\n\n.visibility-notice {\n padding: 0.5em;\n border: 1px solid $fallback--faint;\n border: 1px solid var(--faint, $fallback--faint);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n}\n\n.notice-dismissible {\n padding-right: 4rem;\n position: relative;\n\n .dismiss {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.5em;\n color: inherit;\n }\n}\n\n.fa-scale-110 {\n &.svg-inline--fa,\n &.iconLetter {\n font-size: 1.1em;\n }\n}\n\n.fa-old-padding {\n &.iconLetter,\n &.svg-inline--fa,\n &-layer {\n padding: 0 0.3em;\n }\n}\n\n.veryfaint {\n opacity: 0.25;\n}\n\n.login-hint {\n text-align: center;\n\n @media all and (min-width: 801px) {\n display: none;\n }\n\n a {\n display: inline-block;\n padding: 1em 0;\n width: 100%;\n }\n}\n\n.btn.button-default {\n min-height: 2em;\n}\n\n.new-status-notification {\n position: relative;\n font-size: 1.1em;\n z-index: 1;\n flex: 1;\n}\n\n@media all and (max-width: 800px) {\n .mobile-hidden {\n display: none;\n }\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(359deg);\n }\n}\n\n@keyframes shakeError {\n 0% {\n transform: translateX(0);\n }\n\n 15% {\n transform: translateX(0.375rem);\n }\n\n 30% {\n transform: translateX(-0.375rem);\n }\n\n 45% {\n transform: translateX(0.375rem);\n }\n\n 60% {\n transform: translateX(-0.375rem);\n }\n\n 75% {\n transform: translateX(0.375rem);\n }\n\n 90% {\n transform: translateX(-0.375rem);\n }\n\n 100% {\n transform: translateX(0);\n }\n}\n\n// Vue transitions\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.3s;\n}\n\n.fade-enter-from,\n.fade-leave-active {\n opacity: 0;\n}\n/* stylelint-enable no-descending-specificity */\n\n.visible-for-screenreader-only {\n display: block;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n visibility: visible;\n clip: rect(0 0 0 0);\n padding: 0;\n position: absolute;\n}\n","/* stylelint-disable no-descending-specificity */\n.panel {\n position: relative;\n display: flex;\n flex-direction: column;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n\n &::after,\n & {\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n\n &::after {\n content: \"\";\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 5;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n pointer-events: none;\n }\n}\n\n.panel-body {\n padding: var(--panel-body-padding, 0);\n\n &:empty::before {\n content: \"¯\\\\_(ツ)_/¯\"; // Could use words but it'd require translations\n display: block;\n margin: 1em;\n text-align: center;\n }\n\n > p {\n line-height: 1.3;\n padding: 1em;\n margin: 0;\n }\n}\n\n.panel-heading,\n.panel-footer {\n --panel-heading-height-padding: 0.6em;\n --__panel-heading-gap: 0.5em;\n --__panel-heading-height: 3.2em;\n --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));\n\n position: relative;\n box-sizing: border-box;\n display: grid;\n grid-auto-flow: column;\n grid-template-columns: minmax(50%, 1fr);\n grid-auto-columns: auto;\n grid-column-gap: var(--__panel-heading-gap);\n flex: none;\n background-size: cover;\n padding: var(--panel-heading-height-padding);\n height: var(--__panel-heading-height);\n line-height: var(--__panel-heading-height-inner);\n z-index: 4;\n\n &.-flexible-height {\n --__panel-heading-height: auto;\n\n &::after,\n &::before {\n display: none;\n }\n }\n\n &.-stub {\n &,\n &::after {\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n\n &.-sticky {\n position: sticky;\n top: var(--navbar-height);\n }\n\n &::after,\n &::before {\n content: \"\";\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n left: 0;\n pointer-events: none;\n }\n\n .title {\n font-size: 1.3em;\n }\n\n .alert {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow-x: hidden;\n }\n\n &:not(.-flexible-height) {\n > .button-default,\n > .alert {\n height: var(--__panel-heading-height-inner);\n min-height: 0;\n box-sizing: border-box;\n margin: 0;\n min-width: 1px;\n padding-top: 0;\n padding-bottom: 0;\n align-self: stretch;\n }\n }\n}\n\n// TODO Should refactor panels into separate component and utilize slots\n\n.panel-heading {\n border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;\n border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;\n border-width: 0 0 1px;\n align-items: start;\n // panel theme\n color: var(--panelText);\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n\n &::after {\n background-color: $fallback--fg;\n background-color: var(--panel, $fallback--fg);\n z-index: -2;\n border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;\n border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;\n box-shadow: var(--panelHeaderShadow);\n }\n\n a,\n .-link {\n color: $fallback--link;\n color: var(--panelLink, $fallback--link);\n }\n\n .button-unstyled:hover,\n a:hover {\n i[class*=\"icon-\"],\n .svg-inline--fa,\n .iconLetter {\n color: var(--panelText);\n }\n }\n\n .faint {\n background-color: transparent;\n color: $fallback--faint;\n color: var(--panelFaint, $fallback--faint);\n }\n\n .faint-link {\n color: $fallback--faint;\n color: var(--faintLink, $fallback--faint);\n }\n\n &:not(.-flexible-height) {\n > .button-default {\n flex-shrink: 0;\n\n &,\n i[class*=\"icon-\"] {\n color: $fallback--text;\n color: var(--btnPanelText, $fallback--text);\n }\n\n &:active {\n background-color: $fallback--fg;\n background-color: var(--btnPressedPanel, $fallback--fg);\n color: $fallback--text;\n color: var(--btnPressedPanelText, $fallback--text);\n }\n\n &:disabled {\n color: $fallback--text;\n color: var(--btnDisabledPanelText, $fallback--text);\n }\n\n &.toggled {\n color: $fallback--text;\n color: var(--btnToggledPanelText, $fallback--text);\n }\n }\n }\n\n .rightside-button {\n align-self: stretch;\n text-align: center;\n width: var(--__panel-heading-height);\n height: var(--__panel-heading-height);\n margin: calc(-1 * var(--panel-heading-height-padding)) 0;\n margin-right: calc(-1 * var(--__panel-heading-gap));\n\n > button {\n box-sizing: border-box;\n padding: calc(1 * var(--panel-heading-height-padding)) 0;\n height: 100%;\n width: 100%;\n text-align: center;\n\n svg {\n font-size: 1.2em;\n }\n }\n }\n\n .rightside-icon {\n align-self: stretch;\n text-align: center;\n width: var(--__panel-heading-height);\n margin-right: calc(-1 * var(--__panel-heading-gap));\n\n svg {\n font-size: 1.2em;\n }\n }\n}\n\n.panel-footer {\n border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;\n border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);\n align-items: center;\n border-width: 1px 0 0;\n border-style: solid;\n border-color: var(--border, $fallback--border);\n}\n/* stylelint-enable no-descending-specificity */\n","\n@import \"../../variables\";\n\n.thread-tree-replies {\n margin-left: var(--status-margin, $status-margin);\n border-left: 2px solid var(--border, $fallback--border);\n}\n\n.thread-tree-replies-hidden {\n padding: var(--status-margin, $status-margin);\n\n /* Make the button stretch along the whole row */\n display: flex;\n align-items: stretch;\n flex-direction: column;\n}\n","\n@import \"../../variables\";\n\n.Conversation {\n z-index: 1;\n\n .conversation-dive-to-top-level-box {\n padding: var(--status-margin, $status-margin);\n border-bottom: 1px solid var(--border, $fallback--border);\n border-radius: 0;\n\n /* Make the button stretch along the whole row */\n display: flex;\n align-items: stretch;\n flex-direction: column;\n }\n\n .thread-ancestors {\n margin-left: var(--status-margin, $status-margin);\n border-left: 2px solid var(--border, $fallback--border);\n }\n\n .thread-ancestor.-faded .StatusContent {\n --link: var(--faintLink);\n --text: var(--faint);\n\n color: var(--text);\n }\n\n .thread-ancestor-dive-box {\n padding-left: var(--status-margin, $status-margin);\n border-bottom: 1px solid var(--border, $fallback--border);\n border-radius: 0;\n\n /* Make the button stretch along the whole row */\n &,\n &-inner {\n display: flex;\n align-items: stretch;\n flex-direction: column;\n }\n }\n\n .thread-ancestor-dive-box-inner {\n padding: var(--status-margin, $status-margin);\n }\n\n .conversation-status {\n border-bottom: 1px solid var(--border, $fallback--border);\n border-radius: 0;\n }\n\n .thread-ancestor-has-other-replies .conversation-status,\n &:last-child .conversation-status,\n .thread-ancestor:last-child .conversation-status,\n .thread-ancestor:last-child .thread-ancestor-dive-box,\n &.-expanded .thread-tree .conversation-status {\n border-bottom: none;\n }\n\n .thread-ancestors + .thread-tree > .conversation-status {\n border-top: 1px solid var(--border, $fallback--border);\n }\n\n /* expanded conversation in timeline */\n &.status-fadein.-expanded .thread-body {\n border-left: 4px solid $fallback--cRed;\n border-left-color: var(--cRed, $fallback--cRed);\n border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;\n border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);\n border-bottom: 1px solid var(--border, $fallback--border);\n }\n\n &.-expanded.status-fadein {\n margin: calc(var(--status-margin, $status-margin) / 2);\n }\n}\n","\n@import \"../../variables\";\n\n.timeline-menu-popover {\n min-width: 24rem;\n max-width: 100vw;\n margin-top: 0.6rem;\n font-size: 1rem;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n a {\n display: block;\n padding: 0 0.65em;\n height: 3.5em;\n line-height: 3.5em;\n\n &:hover {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: $fallback--link;\n color: var(--selectedMenuText, $fallback--link);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &.router-link-active {\n font-weight: bolder;\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: $fallback--text;\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n\n &:hover {\n text-decoration: underline;\n }\n }\n\n svg {\n margin-right: 0.4em;\n margin-left: -0.2em;\n }\n }\n\n li {\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n padding: 0;\n\n &:last-child a {\n border-bottom-right-radius: $fallback--panelRadius;\n border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);\n border-bottom-left-radius: $fallback--panelRadius;\n border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);\n }\n\n &:last-child {\n border: none;\n }\n }\n}\n\n.TimelineMenu {\n margin-right: auto;\n min-width: 0;\n\n .popover-trigger-button {\n vertical-align: bottom;\n }\n\n .panel::after {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n }\n\n .timeline-menu-title {\n margin: 0;\n cursor: pointer;\n user-select: none;\n width: 100%;\n display: flex;\n\n .timeline-menu-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n svg {\n margin-left: 0.6em;\n transition: transform 100ms;\n }\n\n .click-blocker {\n cursor: default;\n flex-grow: 1;\n }\n }\n\n &.open .timeline-menu-title svg {\n color: $fallback--text;\n color: var(--panelText, $fallback--text);\n transform: rotate(180deg);\n }\n\n .panel {\n box-shadow: var(--popoverShadow);\n }\n}\n","@import \"../../variables\";\n\n.Timeline {\n .alert-dot {\n border-radius: 100%;\n height: 8px;\n width: 8px;\n position: absolute;\n left: calc(50% - 4px);\n top: calc(50% - 4px);\n margin-left: 6px;\n margin-top: -6px;\n background-color: var(--badgeNeutral);\n }\n\n .alert-badge {\n font-size: 0.75em;\n line-height: 1;\n text-align: right;\n border-radius: var(--tooltipRadius);\n position: absolute;\n left: calc(50% - 0.5em);\n top: calc(50% - 0.4em);\n padding: 0.2em;\n margin-left: 0.7em;\n margin-top: -1em;\n background-color: var(--badgeNeutral);\n color: var(--badgeNeutralText);\n }\n\n .loadmore-button {\n position: relative;\n }\n\n &.-blocked {\n cursor: progress;\n }\n\n .conversation-heading {\n top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 2));\n z-index: 2;\n }\n\n &.-nonpanel {\n .timeline-heading {\n text-align: center;\n line-height: 2.75em;\n padding: 0 0.5em;\n\n .button-default,\n .alert {\n line-height: 2em;\n width: 100%;\n }\n }\n }\n}\n","@import \"../../variables\";\n\n/* stylelint-disable no-descending-specificity */\n.tab-switcher {\n display: flex;\n\n .tab-icon {\n margin: 0.2em auto;\n display: block;\n }\n\n &.top-tabs {\n flex-direction: column;\n\n > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n flex-direction: row;\n flex: 0 0 auto;\n\n &::after,\n &::before {\n content: \"\";\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n\n .tab-wrapper {\n height: 2em;\n\n &:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n }\n\n .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: 6px - 99px;\n }\n }\n\n .contents.scrollable-tabs {\n flex-basis: 0;\n }\n }\n\n &.side-tabs {\n flex-direction: row;\n\n @media all and (max-width: 800px) {\n overflow-x: auto;\n }\n\n > .contents {\n flex: 1 1 auto;\n }\n\n > .tabs {\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n flex-direction: column;\n\n &::after,\n &::before {\n flex-shrink: 0;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: $fallback--border;\n border-right-color: var(--border, $fallback--border);\n }\n\n &::after {\n flex-grow: 1;\n }\n\n &::before {\n flex-grow: 0;\n }\n\n .tab-wrapper {\n min-width: 10em;\n display: flex;\n flex-direction: column;\n\n @media all and (max-width: 800px) {\n min-width: 4em;\n }\n\n &:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: $fallback--border;\n border-right-color: var(--border, $fallback--border);\n }\n\n &::before {\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: $fallback--border;\n border-right-color: var(--border, $fallback--border);\n }\n\n &:last-child .tab {\n margin-bottom: 0;\n }\n }\n\n .tab {\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n\n @media all and (max-width: 800px) {\n padding-left: 0.25em;\n padding-right: calc(0.25em + 200px);\n margin-right: calc(0.25em - 200px);\n margin-left: 0.25em;\n\n .text {\n display: none;\n }\n }\n }\n }\n }\n\n .contents {\n flex: 1 0 auto;\n min-height: 0;\n\n .hidden {\n display: none;\n }\n\n .full-height:not(.hidden) {\n height: 100%;\n display: flex;\n flex-direction: column;\n\n > *:not(.mobile-label) {\n flex: 1;\n }\n }\n\n &.scrollable-tabs {\n overflow-y: auto;\n }\n }\n\n .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n\n &:not(.active) {\n z-index: 4;\n\n &:hover {\n z-index: 6;\n }\n }\n\n &.active {\n background: transparent;\n z-index: 5;\n color: $fallback--text;\n color: var(--tabActiveText, $fallback--text);\n }\n\n img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n }\n }\n\n .tabs {\n display: flex;\n position: relative;\n box-sizing: border-box;\n\n &::after,\n &::before {\n display: block;\n flex: 1 1 auto;\n }\n }\n\n .tab-wrapper {\n position: relative;\n display: flex;\n flex: 0 0 auto;\n\n &:not(.active) {\n &::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n }\n }\n }\n\n .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, $fallback--border);\n\n @media all and (min-width: 800px) {\n display: none;\n }\n }\n}\n/* stylelint-enable no-descending-specificity */\n","\n@import \"../../variables\";\n\n.chat-title {\n display: flex;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n\n --emoji-size: 14px;\n\n .username {\n max-width: 100%;\n text-overflow: ellipsis;\n white-space: nowrap;\n display: inline;\n word-wrap: break-word;\n overflow: hidden;\n }\n\n .avatar-container {\n align-self: center;\n line-height: 1;\n }\n\n .titlebar-avatar {\n margin-right: 0.5em;\n height: 1.5em;\n width: 1.5em;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n\n &.animated::before {\n display: none;\n }\n }\n}\n",".chat-list-item {\n display: flex;\n flex-direction: row;\n padding: 0.75em;\n height: 5em;\n overflow: hidden;\n box-sizing: border-box;\n cursor: pointer;\n\n :focus {\n outline: none;\n }\n\n &:hover {\n background-color: var(--selectedPost, $fallback--lightBg);\n box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%);\n }\n\n .chat-list-item-left {\n margin-right: 1em;\n }\n\n .chat-list-item-center {\n width: 100%;\n box-sizing: border-box;\n overflow: hidden;\n word-wrap: break-word;\n }\n\n .heading {\n width: 100%;\n display: inline-flex;\n justify-content: space-between;\n line-height: 1em;\n }\n\n .heading-right {\n white-space: nowrap;\n }\n\n .name-and-account-name {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex-shrink: 1;\n line-height: var(--post-line-height);\n }\n\n .chat-preview {\n display: inline-flex;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n margin: 0.35em 0;\n color: $fallback--text;\n color: var(--faint, $fallback--text);\n width: 100%;\n }\n\n a {\n color: var(--faintLink, $fallback--link);\n text-decoration: none;\n pointer-events: none;\n }\n\n &:hover .animated.avatar {\n canvas {\n display: none;\n }\n\n img {\n visibility: visible;\n }\n }\n\n .Avatar {\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .chat-preview-body {\n --emoji-size: 1.4em;\n\n padding-right: 1em;\n }\n\n .time-wrapper {\n line-height: var(--post-line-height);\n }\n}\n","\n.basic-user-card {\n display: flex;\n flex: 1 0;\n margin: 0;\n padding: 0.6em 1em;\n\n --emoji-size: 14px;\n\n &-collapsed-content {\n margin-left: 0.7em;\n text-align: left;\n flex: 1;\n min-width: 0;\n }\n\n &-user-name {\n img {\n object-fit: contain;\n height: 16px;\n width: 16px;\n vertical-align: middle;\n }\n }\n\n &-user-name-value,\n &-screen-name {\n display: inline-block;\n max-width: 100%;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n\n &-expanded-content {\n flex: 1;\n margin-left: 0.7em;\n min-width: 0;\n }\n}\n",".chat-new {\n .input-wrap {\n display: flex;\n margin: 0.7em 0.5em;\n\n input {\n width: 100%;\n }\n }\n\n .search-icon {\n margin-right: 0.3em;\n }\n\n .member-list {\n padding-bottom: 0.7rem;\n }\n\n .basic-user-card:hover {\n cursor: pointer;\n background-color: var(--selectedPost, $fallback--lightBg);\n }\n\n .go-back-button {\n text-align: center;\n line-height: 1;\n height: 100%;\n align-self: start;\n width: var(--__panel-heading-height-inner);\n }\n}\n","\n@import \"../../variables\";\n\n.chat-list {\n min-height: 25em;\n margin-bottom: 0;\n}\n\n.emtpy-chat-list-alert {\n padding: 3em;\n font-size: 1.2em;\n display: flex;\n justify-content: center;\n color: $fallback--text;\n color: var(--faint, $fallback--text);\n}\n\n","@import \"../../variables\";\n\n.chat-message-wrapper {\n &.hovered-message-chain {\n .animated.Avatar {\n canvas {\n display: none;\n }\n\n img {\n visibility: visible;\n }\n }\n }\n\n .chat-message-menu {\n transition: opacity 0.1s;\n opacity: 0;\n position: absolute;\n top: -0.8em;\n\n button {\n padding-top: 0.2em;\n padding-bottom: 0.2em;\n }\n }\n\n .menu-icon {\n cursor: pointer;\n\n &:hover,\n .extra-button-popover.open & {\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n }\n\n .popover {\n width: 12em;\n }\n\n .chat-message {\n display: flex;\n padding-bottom: 0.5em;\n\n .status-body:hover {\n --_still-image-img-visibility: visible;\n --_still-image-canvas-visibility: hidden;\n --_still-image-label-visibility: hidden;\n }\n }\n\n .avatar-wrapper {\n margin-right: 0.72em;\n width: 32px;\n }\n\n .link-preview,\n .attachments {\n margin-bottom: 1em;\n }\n\n .status {\n border-radius: $fallback--chatMessageRadius;\n border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);\n display: flex;\n padding: 0.75em;\n }\n\n .created-at {\n position: relative;\n float: right;\n font-size: 0.8em;\n margin: -1em 0 -0.5em;\n font-style: italic;\n opacity: 0.8;\n }\n\n .without-attachment {\n .message-content {\n // TODO figure out how to do it properly\n .RichContent::after {\n margin-right: 5.4em;\n content: \" \";\n display: inline-block;\n }\n }\n }\n\n .pending {\n .status-content.media-body,\n .created-at {\n color: var(--faint);\n }\n }\n\n .error {\n .status-content.media-body,\n .created-at {\n color: $fallback--cRed;\n color: var(--badgeNotification, $fallback--cRed);\n }\n }\n\n .chat-message-inner {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n max-width: 80%;\n min-width: 10em;\n width: 100%;\n }\n\n .outgoing {\n display: flex;\n flex-flow: row wrap;\n align-content: end;\n justify-content: flex-end;\n\n a {\n color: var(--chatMessageOutgoingLink, $fallback--link);\n }\n\n .status {\n color: var(--chatMessageOutgoingText, $fallback--text);\n background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);\n border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);\n }\n\n .chat-message-inner {\n align-items: flex-end;\n }\n\n .chat-message-menu {\n right: 0.4rem;\n }\n }\n\n .incoming {\n a {\n color: var(--chatMessageIncomingLink, $fallback--link);\n }\n\n .status {\n color: var(--chatMessageIncomingText, $fallback--text);\n background-color: var(--chatMessageIncomingBg, $fallback--bg);\n border: 1px solid var(--chatMessageIncomingBorder, --border);\n }\n\n .created-at {\n a {\n color: var(--chatMessageIncomingText, $fallback--text);\n }\n }\n\n .chat-message-menu {\n left: 0.4rem;\n }\n }\n\n .chat-message-inner.with-media {\n width: 100%;\n\n .status {\n width: 100%;\n }\n }\n\n .visible {\n opacity: 1;\n }\n}\n\n.chat-message-date-separator {\n text-align: center;\n margin: 1.4em 0;\n font-size: 0.9em;\n user-select: none;\n color: $fallback--text;\n color: var(--faintedText, $fallback--text);\n}\n",".chat-view {\n display: flex;\n height: 100%;\n\n .chat-view-inner {\n height: auto;\n width: 100%;\n overflow: visible;\n display: flex;\n }\n\n .chat-view-body {\n box-sizing: border-box;\n background-color: var(--chatBg, $fallback--bg);\n display: flex;\n flex-direction: column;\n width: 100%;\n overflow: visible;\n min-height: calc(100vh - var(--navbar-height));\n margin: 0;\n border-radius: 10px 10px 0 0;\n border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;\n\n &::after {\n border-radius: 0;\n }\n }\n\n .message-list {\n padding: 0 0.8em;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: end;\n }\n\n .footer {\n position: sticky;\n bottom: 0;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n z-index: 1;\n }\n\n .chat-view-heading {\n grid-template-columns: auto minmax(50%, 1fr);\n }\n\n .go-back-button {\n text-align: center;\n line-height: 1;\n height: 100%;\n align-self: start;\n width: var(--__panel-heading-height-inner);\n }\n\n .jump-to-bottom-button {\n width: 2.5em;\n height: 2.5em;\n border-radius: 100%;\n position: absolute;\n right: 1.3em;\n top: -3.2em;\n background-color: $fallback--fg;\n background-color: var(--btn, $fallback--fg);\n display: flex;\n justify-content: center;\n align-items: center;\n box-shadow: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 4px rgb(0 0 0 / 30%);\n z-index: 10;\n transition: 0.35s all;\n transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n opacity: 0;\n visibility: hidden;\n cursor: pointer;\n\n &.visible {\n opacity: 1;\n visibility: visible;\n }\n\n i {\n font-size: 1em;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n\n .unread-message-count {\n font-size: 0.8em;\n left: 50%;\n margin-top: -1rem;\n padding: 0.1em;\n border-radius: 50px;\n position: absolute;\n }\n\n .chat-loading-error {\n width: 100%;\n display: flex;\n align-items: flex-end;\n height: 100%;\n\n .error {\n width: 100%;\n }\n }\n }\n}\n","\n.follow-card {\n &-content-container {\n flex-shrink: 0;\n display: flex;\n flex-flow: row wrap;\n justify-content: space-between;\n line-height: 1.5em;\n }\n\n &-button {\n margin-top: 0.5em;\n padding: 0 1.5em;\n margin-left: 1em;\n }\n\n &-follow-button {\n margin-top: 0.5em;\n margin-left: auto;\n width: 10em;\n }\n}\n","@import \"../../variables\";\n\n.with-load-more {\n &-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: $fallback--border;\n border-top-color: var(--border, $fallback--border);\n\n .error {\n font-size: 1rem;\n }\n\n a {\n cursor: pointer;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.user-profile {\n flex: 2;\n flex-basis: 500px;\n\n // No sticky header on user profile\n --currentPanelStack: 1;\n\n .user-birthday {\n margin: 0 0.75em 0.5em;\n }\n\n .user-profile-fields {\n margin: 0 0.5em;\n\n img {\n object-fit: contain;\n vertical-align: middle;\n max-width: 100%;\n max-height: 400px;\n\n &.emoji {\n width: 18px;\n height: 18px;\n }\n }\n\n .user-profile-field {\n display: flex;\n margin: 0.25em;\n border: 1px solid var(--border, $fallback--border);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .user-profile-field-name {\n flex: 0 1 30%;\n font-weight: 500;\n text-align: right;\n color: var(--lightText);\n min-width: 120px;\n border-right: 1px solid var(--border, $fallback--border);\n }\n\n .user-profile-field-value {\n flex: 1 1 70%;\n color: var(--text);\n margin: 0 0 0 0.25em;\n }\n\n .user-profile-field-name,\n .user-profile-field-value {\n line-height: 1.3;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n padding: 0.5em 1.5em;\n box-sizing: border-box;\n }\n }\n }\n\n .userlist-placeholder {\n display: flex;\n justify-content: center;\n align-items: middle;\n padding: 2em;\n }\n}\n\n.user-profile-placeholder {\n .panel-body {\n display: flex;\n justify-content: center;\n align-items: middle;\n padding: 7em;\n }\n}\n","\n@import \"../../variables\";\n\n.search-result-heading {\n color: $fallback--faint;\n color: var(--faint, $fallback--faint);\n padding: 0.75rem;\n text-align: center;\n}\n\n@media all and (max-width: 800px) {\n .search-nav-heading {\n .tab-switcher .tabs .tab-wrapper {\n display: block;\n justify-content: center;\n flex: 1 1 auto;\n text-align: center;\n }\n }\n}\n\n.search-result {\n box-sizing: border-box;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n}\n\n.search-result-footer {\n border-width: 1px 0 0;\n border-style: solid;\n border-color: var(--border, $fallback--border);\n padding: 10px;\n background-color: $fallback--fg;\n background-color: var(--panel, $fallback--fg);\n}\n\n.search-input-container {\n padding: 0.8rem;\n display: flex;\n justify-content: center;\n\n .search-input {\n width: 100%;\n line-height: 1.125rem;\n font-size: 1rem;\n padding: 0.5rem;\n box-sizing: border-box;\n }\n\n .search-button {\n margin-left: 0.5em;\n }\n}\n\n.loading-icon {\n padding: 1em;\n}\n\n.trend {\n display: flex;\n align-items: center;\n\n .hashtag {\n flex: 1 1 auto;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .count {\n flex: 0 0 auto;\n width: 2rem;\n font-size: 1.5rem;\n line-height: 2.25rem;\n font-weight: 500;\n text-align: center;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n}\n\n.more-statuses-button {\n height: 3.5em;\n line-height: 3.5em;\n}\n\n","\n@import \"../../variables\";\n\n.interface-language-switcher {\n .language-select {\n margin-right: 1em;\n }\n}\n","\n@import \"../../variables\";\n$validations-cRed: #f04124;\n\n.registration-form {\n display: flex;\n flex-direction: column;\n margin: 0.6em;\n\n .container {\n display: flex;\n flex-direction: row;\n\n > * {\n min-width: 0;\n }\n }\n\n .terms-of-service {\n flex: 0 1 50%;\n margin: 0.8em;\n }\n\n .text-fields {\n margin-top: 0.6em;\n flex: 1 0;\n display: flex;\n flex-direction: column;\n }\n\n textarea {\n min-height: 100px;\n resize: vertical;\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n padding: 0.3em 0;\n line-height: 2;\n margin-bottom: 1em;\n }\n\n .form-group--error {\n animation-name: shakeError;\n animation-duration: 0.6s;\n animation-timing-function: ease-in-out;\n }\n\n .form-group--error .form--label {\n color: $validations-cRed;\n color: var(--cRed, $validations-cRed);\n }\n\n .form-error {\n margin-top: -0.7em;\n text-align: left;\n\n span {\n font-size: 0.85em;\n }\n }\n\n .form-error ul {\n list-style: none;\n padding: 0 0 0 5px;\n margin-top: 0;\n\n li::before {\n content: \"• \";\n }\n }\n\n form textarea {\n line-height: 16px;\n resize: vertical;\n }\n\n .captcha {\n max-width: 350px;\n margin-bottom: 0.4em;\n }\n\n .btn {\n margin-top: 0.6em;\n height: 2em;\n }\n\n .error {\n text-align: center;\n }\n}\n\n@media all and (max-width: 800px) {\n .registration-form .container {\n flex-direction: column-reverse;\n }\n}\n","\n@import \"../../variables\";\n\n.password-reset-form {\n display: flex;\n flex-direction: column;\n align-items: center;\n margin: 0.6em;\n\n .container {\n display: flex;\n flex: 1 0;\n flex-direction: column;\n margin-top: 0.6em;\n max-width: 18rem;\n\n > * {\n min-width: 0;\n }\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n margin-bottom: 1em;\n padding: 0.3em 0;\n line-height: 1.85em;\n }\n\n .error {\n text-align: center;\n animation-name: shakeError;\n animation-duration: 0.4s;\n animation-timing-function: ease-in-out;\n }\n\n .alert {\n padding: 0.5em;\n margin: 0.3em 0 1em;\n }\n\n .password-reset-required {\n background-color: var(--alertError, $fallback--alertError);\n padding: 10px 0;\n }\n\n .notice-dismissible {\n padding-right: 2rem;\n }\n\n .dismiss {\n cursor: pointer;\n }\n}\n\n","\n.follow-request-card-content-container {\n display: flex;\n flex-flow: row wrap;\n\n button {\n margin-top: 0.5em;\n margin-right: 0.5em;\n flex: 1 1;\n max-width: 12em;\n min-width: 8em;\n\n &:last-child {\n margin-right: 0;\n }\n }\n}\n","\n.tos-content {\n margin: 1em;\n}\n","\n.staff-group {\n padding-left: 1em;\n padding-top: 1em;\n\n .basic-user-card {\n padding-left: 0;\n }\n}\n\n",".mrf-section {\n margin: 1em;\n\n table {\n width: 100%;\n text-align: left;\n padding-left: 10px;\n padding-bottom: 20px;\n\n th,\n td {\n width: 180px;\n max-width: 360px;\n overflow: hidden;\n vertical-align: text-top;\n }\n\n th + th,\n td + td {\n width: auto;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.list-card {\n display: flex;\n}\n\n.list-name {\n flex-grow: 1;\n}\n\n.list-name,\n.button-list-edit {\n margin: 0;\n padding: 1em;\n color: $fallback--link;\n color: var(--link, $fallback--link);\n\n &:hover {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: $fallback--link;\n color: var(--selectedMenuText, $fallback--link);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n }\n}\n","\n.Lists {\n .new-list-button {\n padding: 0 0.5em;\n }\n}\n","\n@import \"../../variables\";\n\n.ListsUserSearch {\n .input-wrap {\n display: flex;\n margin: 0.7em 0.5em;\n\n input {\n width: 100%;\n }\n }\n\n .search-icon {\n margin-right: 0.3em;\n }\n}\n\n","\n@import \"src/variables\";\n\n.panel-loading {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n font-size: 2em;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n\n .loading-text svg {\n line-height: 0;\n vertical-align: middle;\n color: $fallback--text;\n color: var(--text, $fallback--text);\n }\n}\n","\n@import \"../../variables\";\n\n.ListEdit {\n --panel-body-padding: 0.5em;\n\n height: calc(100vh - var(--navbar-height));\n overflow: hidden;\n display: flex;\n flex-direction: column;\n\n .list-edit-heading {\n grid-template-columns: auto minmax(50%, 1fr);\n }\n\n .panel-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n overflow: hidden;\n }\n\n .list-member-management {\n flex: 1 0 auto;\n }\n\n .search-icon {\n margin-right: 0.3em;\n }\n\n .users-list {\n padding-bottom: 0.7rem;\n overflow-y: auto;\n }\n\n & .search-list,\n & .members-list {\n overflow: hidden;\n flex-direction: column;\n min-height: 0;\n }\n\n .go-back-button {\n text-align: center;\n line-height: 1;\n height: 100%;\n align-self: start;\n width: var(--__panel-heading-height-inner);\n }\n\n .btn {\n margin: 0 0.5em;\n }\n\n .panel-footer {\n grid-template-columns: minmax(10%, 1fr);\n\n .footer-button {\n min-width: 9em;\n }\n }\n}\n","\n.announcement-editor {\n display: flex;\n align-items: stretch;\n flex-direction: column;\n\n .announcement-metadata {\n margin-top: 0.5em;\n }\n\n .post-textarea {\n resize: vertical;\n height: 10em;\n overflow: none;\n box-sizing: content-box;\n }\n}\n","\n@import \"../../variables\";\n\n.announcement {\n border-bottom: 1px solid var(--border, $fallback--border);\n border-radius: 0;\n padding: var(--status-margin, $status-margin);\n\n .heading,\n .body {\n margin-bottom: var(--status-margin, $status-margin);\n }\n\n .footer {\n display: flex;\n flex-direction: column;\n\n .times {\n display: flex;\n flex-direction: column;\n }\n }\n\n .footer .actions {\n display: flex;\n flex-direction: row;\n justify-content: space-evenly;\n\n .btn {\n flex: 1;\n margin: 1em;\n max-width: 10em;\n }\n }\n}\n","\n@import \"../../variables\";\n\n.announcements-page {\n .post-form {\n padding: var(--status-margin, $status-margin);\n\n .heading,\n .body {\n margin-bottom: var(--status-margin, $status-margin);\n }\n\n .post-button {\n min-width: 10em;\n }\n }\n}\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/emoji.json b/priv/static/static/emoji.json
index 12b91b3f6..6be645825 100644
--- a/priv/static/static/emoji.json
+++ b/priv/static/static/emoji.json
@@ -1,1431 +1 @@
-{
- "100": "💯",
- "1234": "🔢",
- "1st_place_medal": "🥇",
- "2nd_place_medal": "🥈",
- "3rd_place_medal": "🥉",
- "8ball": "🎱",
- "a_button_blood_type": "🅰",
- "ab": "🆎",
- "abacus": "🧮",
- "abc": "🔤",
- "abcd": "🔡",
- "accept": "🉑",
- "adhesive_bandage": "🩹",
- "admission_tickets": "🎟",
- "adult": "🧑",
- "aerial_tramway": "🚡",
- "airplane": "✈",
- "airplane_arriving": "🛬",
- "airplane_departure": "🛫",
- "alarm_clock": "⏰",
- "alembic": "⚗️",
- "alien": "👽",
- "ambulance": "🚑",
- "amphora": "🏺",
- "anchor": "⚓",
- "angel": "👼",
- "anger": "💢",
- "anger_right": "🗯",
- "angry": "😠",
- "anguished": "😧",
- "ant": "🐜",
- "apple": "🍎",
- "aquarius": "♒",
- "aries": "♈",
- "arrow_backward": "◀️",
- "arrow_double_down": "⏬",
- "arrow_double_up": "⏫",
- "arrow_down": "⬇️",
- "arrow_down_small": "🔽",
- "arrow_forward": "▶️",
- "arrow_heading_down": "⤵️",
- "arrow_heading_up": "⤴️",
- "arrow_left": "⬅️",
- "arrow_lower_left": "↙️",
- "arrow_lower_right": "↘️",
- "arrow_right": "➡",
- "arrow_right_hook": "↪️",
- "arrow_up": "⬆️",
- "arrow_up_down": "↕",
- "arrow_up_small": "🔼",
- "arrow_upper_left": "↖",
- "arrow_upper_right": "↗️",
- "arrows_clockwise": "🔃",
- "arrows_counterclockwise": "🔄",
- "art": "🎨",
- "articulated_lorry": "🚛",
- "artist_palette": "🎨",
- "asterisk": "*⃣",
- "astonished": "😲",
- "athletic_shoe": "👟",
- "atm": "🏧",
- "atom": "⚛",
- "atom_symbol": "⚛️",
- "auto_rickshaw": "🛺",
- "automobile": "🚗",
- "avocado": "🥑",
- "axe": "🪓",
- "b_button_blood_type": "🅱",
- "baby": "👶",
- "baby_bottle": "🍼",
- "baby_chick": "🐤",
- "baby_symbol": "🚼",
- "back": "🔙",
- "bacon": "🥓",
- "badger": "🦡",
- "badminton": "🏸",
- "bagel": "🥯",
- "baggage_claim": "🛄",
- "baguette_bread": "🥖",
- "balance_scale": "⚖️",
- "bald": "🦲",
- "ballet_shoes": "🩰",
- "balloon": "🎈",
- "ballot_box": "🗳",
- "ballot_box_with_check": "☑️",
- "bamboo": "🎍",
- "banana": "🍌",
- "bangbang": "‼️",
- "banjo": "🪕",
- "bank": "🏦",
- "bar_chart": "📊",
- "barber": "💈",
- "baseball": "⚾",
- "basket": "🧺",
- "basketball": "🏀",
- "basketballer": "⛹",
- "bat": "🦇",
- "bath": "🛀",
- "bathtub": "🛁",
- "battery": "🔋",
- "beach_umbrella": "⛱",
- "beach_with_umbrella": "🏖",
- "bear": "🐻",
- "beard": "🧔",
- "bearded_person": "🧔",
- "bed": "🛏",
- "bee": "🐝",
- "beer": "🍺",
- "beers": "🍻",
- "beetle": "🐞",
- "beginner": "🔰",
- "bell": "🔔",
- "bellhop_bell": "🛎",
- "bento": "🍱",
- "beverage_box": "🧃",
- "bicyclist": "🚴",
- "bike": "🚲",
- "bikini": "👙",
- "billed_cap": "🧢",
- "biohazard": "☣️",
- "bird": "🐦",
- "birthday": "🎂",
- "black_circle": "⚫",
- "black_heart": "🖤",
- "black_joker": "🃏",
- "black_large_square": "⬛",
- "black_medium_small_square": "◾",
- "black_medium_square": "◼",
- "black_nib": "✒️",
- "black_small_square": "▪",
- "black_square_button": "🔲",
- "blond_haired_person": "👱",
- "blossom": "🌼",
- "blowfish": "🐡",
- "blue_book": "📘",
- "blue_car": "🚙",
- "blue_circle": "🔵",
- "blue_heart": "💙",
- "blue_square": "🟦",
- "blush": "😊",
- "boar": "🐗",
- "bomb": "💣",
- "bone": "🦴",
- "book": "📖",
- "bookmark": "🔖",
- "bookmark_tabs": "📑",
- "books": "📚",
- "boom": "💥",
- "boot": "👢",
- "bouquet": "💐",
- "bow": "🙇",
- "bow_and_arrow": "🏹",
- "bowl_with_spoon": "🥣",
- "bowling": "🎳",
- "boxing_glove": "🥊",
- "boy": "👦",
- "brain": "🧠",
- "bread": "🍞",
- "breast_feeding": "🤱",
- "breastfeeding": "🤱",
- "brick": "🧱",
- "bride_with_veil": "👰",
- "bridge_at_night": "🌉",
- "briefcase": "💼",
- "briefs": "🩲",
- "broccoli": "🥦",
- "broken_heart": "💔",
- "broom": "🧹",
- "brown_circle": "🟤",
- "brown_heart": "🤎",
- "bug": "🐛",
- "building_construction": "🏗",
- "bulb": "💡",
- "bullettrain_front": "🚅",
- "bullettrain_side": "🚄",
- "burrito": "🌯",
- "bus": "🚌",
- "busstop": "🚏",
- "bust_in_silhouette": "👤",
- "busts_in_silhouette": "👥",
- "butter": "🧈",
- "butterfly": "🦋",
- "cactus": "🌵",
- "cake": "🍰",
- "calendar": "📆",
- "call_me": "🤙",
- "call_me_hand": "🤙",
- "calling": "📲",
- "camel": "🐫",
- "camera": "📷",
- "camera_with_flash": "📸",
- "camping": "🏕",
- "cancer": "♋",
- "candle": "🕯",
- "candy": "🍬",
- "canned_food": "🥫",
- "canoe": "🛶",
- "capital_abcd": "🔠",
- "capricorn": "♑",
- "card_file_box": "🗃",
- "card_index": "📇",
- "card_index_dividers": "🗂",
- "carousel_horse": "🎠",
- "carrot": "🥕",
- "cat": "🐱",
- "cat2": "🐈",
- "cd": "💿",
- "chains": "⛓️",
- "chair": "🪑",
- "champagne": "🍾",
- "champagne_glass": "🥂",
- "chart": "💹",
- "chart_with_downwards_trend": "📉",
- "chart_with_upwards_trend": "📈",
- "check_box_with_check": "☑",
- "check_mark": "✔",
- "checkered_flag": "🏁",
- "cheese": "🧀",
- "cheese_wedge": "🧀",
- "cherries": "🍒",
- "cherry_blossom": "🌸",
- "chess_pawn": "♟",
- "chestnut": "🌰",
- "chicken": "🐔",
- "child": "🧒",
- "children_crossing": "🚸",
- "chipmunk": "🐿",
- "chocolate_bar": "🍫",
- "chopsticks": "🥢",
- "christmas_tree": "🎄",
- "church": "⛪",
- "cinema": "🎦",
- "circled_m": "Ⓜ",
- "circus_tent": "🎪",
- "city_dusk": "🌆",
- "city_sunset": "🌇",
- "cityscape": "🏙",
- "cityscape_at_dusk": "🌆",
- "cl": "🆑",
- "clap": "👏",
- "clapper": "🎬",
- "classical_building": "🏛",
- "clinking_glasses": "🥂",
- "clipboard": "📋",
- "clock1": "🕐",
- "clock10": "🕙",
- "clock1030": "🕥",
- "clock11": "🕚",
- "clock1130": "🕦",
- "clock12": "🕛",
- "clock1230": "🕧",
- "clock130": "🕜",
- "clock2": "🕑",
- "clock230": "🕝",
- "clock3": "🕒",
- "clock330": "🕞",
- "clock4": "🕓",
- "clock430": "🕟",
- "clock5": "🕔",
- "clock530": "🕠",
- "clock6": "🕕",
- "clock630": "🕡",
- "clock7": "🕖",
- "clock730": "🕢",
- "clock8": "🕗",
- "clock830": "🕣",
- "clock9": "🕘",
- "clock930": "🕤",
- "closed_book": "📕",
- "closed_lock_with_key": "🔐",
- "closed_umbrella": "🌂",
- "cloud": "☁️",
- "cloud_with_lightning": "🌩",
- "cloud_with_lightning_and_rain": "⛈️",
- "cloud_with_rain": "🌧",
- "cloud_with_snow": "🌨",
- "clown": "🤡",
- "clown_face": "🤡",
- "club_suit": "♣️",
- "clubs": "♣",
- "coat": "🧥",
- "cocktail": "🍸",
- "coconut": "🥥",
- "coffee": "☕",
- "coffin": "⚰️",
- "cold_face": "🥶",
- "cold_sweat": "😰",
- "comet": "☄️",
- "compass": "🧭",
- "compression": "🗜",
- "computer": "💻",
- "computer_mouse": "🖱",
- "confetti_ball": "🎊",
- "confounded": "😖",
- "confused": "😕",
- "congratulations": "㊗",
- "construction": "🚧",
- "construction_worker": "👷",
- "control_knobs": "🎛",
- "convenience_store": "🏪",
- "cookie": "🍪",
- "cooking": "🍳",
- "cool": "🆒",
- "cop": "👮",
- "copyright": "©",
- "corn": "🌽",
- "couch_and_lamp": "🛋",
- "couple": "👫",
- "couple_with_heart": "💑",
- "couplekiss": "💏",
- "cow": "🐮",
- "cow2": "🐄",
- "cowboy": "🤠",
- "cowboy_hat_face": "🤠",
- "crab": "🦀",
- "crayon": "🖍",
- "crazy_face": "🤪",
- "credit_card": "💳",
- "crescent_moon": "🌙",
- "cricket": "🦗",
- "cricket_game": "🏏",
- "crocodile": "🐊",
- "croissant": "🥐",
- "cross": "✝️",
- "crossed_fingers": "🤞",
- "crossed_flags": "🎌",
- "crossed_swords": "⚔️",
- "crown": "👑",
- "cry": "😢",
- "crying_cat_face": "😿",
- "crystal_ball": "🔮",
- "cucumber": "🥒",
- "cup_with_straw": "🥤",
- "cupcake": "🧁",
- "cupid": "💘",
- "curling_stone": "🥌",
- "curly_hair": "🦱",
- "curly_loop": "➰",
- "currency_exchange": "💱",
- "curry": "🍛",
- "custard": "🍮",
- "customs": "🛃",
- "cut_of_meat": "🥩",
- "cyclone": "🌀",
- "dagger": "🗡",
- "dancer": "💃",
- "dancers": "👯",
- "dango": "🍡",
- "dark_skin_tone": "🏿",
- "dark_sunglasses": "🕶",
- "dart": "🎯",
- "dash": "💨",
- "date": "📅",
- "deaf_person": "🧏",
- "deciduous_tree": "🌳",
- "deer": "🦌",
- "department_store": "🏬",
- "derelict_house": "🏚",
- "desert": "🏜",
- "desert_island": "🏝",
- "desktop_computer": "🖥",
- "detective": "🕵",
- "diamond_shape_with_a_dot_inside": "💠",
- "diamond_suit": "♦️",
- "diamonds": "♦",
- "disappointed": "😞",
- "disappointed_relieved": "😥",
- "diving_mask": "🤿",
- "diya_lamp": "🪔",
- "dizzy": "💫",
- "dizzy_face": "😵",
- "dna": "🧬",
- "do_not_litter": "🚯",
- "dog": "🐶",
- "dog2": "🐕",
- "dollar": "💵",
- "dolls": "🎎",
- "dolphin": "🐬",
- "door": "🚪",
- "double_exclamation_mark": "‼",
- "doughnut": "🍩",
- "dove": "🕊",
- "down_arrow": "⬇",
- "downleft_arrow": "↙",
- "downright_arrow": "↘",
- "dragon": "🐉",
- "dragon_face": "🐲",
- "dress": "👗",
- "dromedary_camel": "🐪",
- "drooling_face": "🤤",
- "drop_of_blood": "🩸",
- "droplet": "💧",
- "drum": "🥁",
- "duck": "🦆",
- "dumpling": "🥟",
- "dvd": "📀",
- "e-mail": "📧",
- "eagle": "🦅",
- "ear": "👂",
- "ear_of_rice": "🌾",
- "ear_with_hearing_aid": "🦻",
- "earth_africa": "🌍",
- "earth_americas": "🌎",
- "earth_asia": "🌏",
- "egg": "🥚",
- "eggplant": "🍆",
- "eight": "8⃣",
- "eight_pointed_black_star": "✴️",
- "eight_spoked_asterisk": "✳️",
- "eightpointed_star": "✴",
- "eightspoked_asterisk": "✳",
- "eject_button": "⏏",
- "electric_plug": "🔌",
- "elephant": "🐘",
- "elf": "🧝",
- "end": "🔚",
- "envelope": "✉",
- "envelope_with_arrow": "📩",
- "euro": "💶",
- "european_castle": "🏰",
- "european_post_office": "🏤",
- "evergreen_tree": "🌲",
- "exclamation": "❗",
- "exclamation_question_mark": "⁉",
- "exploding_head": "🤯",
- "expressionless": "😑",
- "eye": "👁",
- "eyeglasses": "👓",
- "eyes": "👀",
- "face_vomiting": "🤮",
- "face_with_hand_over_mouth": "🤭",
- "face_with_headbandage": "🤕",
- "face_with_monocle": "🧐",
- "face_with_raised_eyebrow": "🤨",
- "face_with_symbols_on_mouth": "🤬",
- "face_with_symbols_over_mouth": "🤬",
- "face_with_thermometer": "🤒",
- "factory": "🏭",
- "fairy": "🧚",
- "falafel": "🧆",
- "fallen_leaf": "🍂",
- "family": "👪",
- "fast_forward": "⏩",
- "fax": "📠",
- "fearful": "😨",
- "feet": "🐾",
- "female_sign": "♀",
- "ferris_wheel": "🎡",
- "ferry": "⛴️",
- "field_hockey": "🏑",
- "file_cabinet": "🗄",
- "file_folder": "📁",
- "film_frames": "🎞",
- "film_projector": "📽",
- "fingers_crossed": "🤞",
- "fire": "🔥",
- "fire_engine": "🚒",
- "fire_extinguisher": "🧯",
- "firecracker": "🧨",
- "fireworks": "🎆",
- "first_place": "🥇",
- "first_quarter_moon": "🌓",
- "first_quarter_moon_with_face": "🌛",
- "fish": "🐟",
- "fish_cake": "🍥",
- "fishing_pole_and_fish": "🎣",
- "fist": "✊",
- "five": "5⃣",
- "flag_black": "🏴",
- "flag_white": "🏳",
- "flags": "🎏",
- "flamingo": "🦩",
- "flashlight": "🔦",
- "flat_shoe": "🥿",
- "fleur-de-lis": "⚜",
- "fleurde-lis": "⚜️",
- "floppy_disk": "💾",
- "flower_playing_cards": "🎴",
- "flushed": "😳",
- "flying_disc": "🥏",
- "flying_saucer": "🛸",
- "fog": "🌫",
- "foggy": "🌁",
- "foot": "🦶",
- "football": "🏈",
- "footprints": "👣",
- "fork_and_knife": "🍴",
- "fork_and_knife_with_plate": "🍽",
- "fortune_cookie": "🥠",
- "fountain": "⛲",
- "fountain_pen": "🖋",
- "four": "4⃣",
- "four_leaf_clover": "🍀",
- "fox": "🦊",
- "framed_picture": "🖼",
- "free": "🆓",
- "french_bread": "🥖",
- "fried_shrimp": "🍤",
- "fries": "🍟",
- "frog": "🐸",
- "frowning": "😦",
- "frowning_face": "☹️",
- "fuelpump": "⛽",
- "full_moon": "🌕",
- "full_moon_with_face": "🌝",
- "funeral_urn": "⚱️",
- "game_die": "🎲",
- "garlic": "🧄",
- "gear": "⚙️",
- "gem": "💎",
- "gemini": "♊",
- "genie": "🧞",
- "ghost": "👻",
- "gift": "🎁",
- "gift_heart": "💝",
- "giraffe": "🦒",
- "girl": "👧",
- "glass_of_milk": "🥛",
- "globe_with_meridians": "🌐",
- "gloves": "🧤",
- "goal": "🥅",
- "goal_net": "🥅",
- "goat": "🐐",
- "goggles": "🥽",
- "golf": "⛳",
- "golfer": "🏌",
- "gorilla": "🦍",
- "grapes": "🍇",
- "green_apple": "🍏",
- "green_book": "📗",
- "green_circle": "🟢",
- "green_heart": "💚",
- "green_salad": "🥗",
- "green_square": "🟩",
- "grey_exclamation": "❕",
- "grey_question": "❔",
- "grimacing": "😬",
- "grin": "😁",
- "grinning": "😀",
- "guard": "💂",
- "guardsman": "💂",
- "guide_dog": "🦮",
- "guitar": "🎸",
- "gun": "🔫",
- "haircut": "💇",
- "hamburger": "🍔",
- "hammer": "🔨",
- "hammer_and_pick": "⚒️",
- "hammer_and_wrench": "🛠",
- "hamster": "🐹",
- "hand_with_fingers_splayed": "🖐",
- "handbag": "👜",
- "handshake": "🤝",
- "hash": "#⃣",
- "hatched_chick": "🐥",
- "hatching_chick": "🐣",
- "head_bandage": "🤕",
- "headphones": "🎧",
- "hear_no_evil": "🙉",
- "heart": "❤️",
- "heart_decoration": "💟",
- "heart_exclamation": "❣",
- "heart_eyes": "😍",
- "heart_eyes_cat": "😻",
- "heart_suit": "♥️",
- "heartbeat": "💓",
- "heartpulse": "💗",
- "hearts": "♥",
- "heavy_check_mark": "✔️",
- "heavy_division_sign": "➗",
- "heavy_dollar_sign": "💲",
- "heavy_minus_sign": "➖",
- "heavy_multiplication_x": "✖️",
- "heavy_plus_sign": "➕",
- "hedgehog": "🦔",
- "helicopter": "🚁",
- "herb": "🌿",
- "hibiscus": "🌺",
- "high_brightness": "🔆",
- "high_heel": "👠",
- "hiking_boot": "🥾",
- "hindu_temple": "🛕",
- "hippopotamus": "🦛",
- "hockey": "🏒",
- "hole": "🕳",
- "honey_pot": "🍯",
- "horse": "🐴",
- "horse_racing": "🏇",
- "hospital": "🏥",
- "hot_face": "🥵",
- "hot_pepper": "🌶",
- "hot_springs": "♨",
- "hotdog": "🌭",
- "hotel": "🏨",
- "hotsprings": "♨️",
- "hourglass": "⌛",
- "hourglass_flowing_sand": "⏳",
- "house": "🏠",
- "house_with_garden": "🏡",
- "houses": "🏘",
- "hugging": "🤗",
- "hundred_points": "💯",
- "hushed": "😯",
- "ice": "🧊",
- "ice_cream": "🍨",
- "ice_hockey": "🏒",
- "ice_skate": "⛸️",
- "icecream": "🍦",
- "id": "🆔",
- "ideograph_advantage": "🉐",
- "imp": "👿",
- "inbox_tray": "📥",
- "incoming_envelope": "📨",
- "index_pointing_up": "☝",
- "infinity": "♾",
- "information": "ℹ️",
- "information_desk_person": "💁",
- "information_source": "ℹ",
- "innocent": "😇",
- "input_numbers": "🔢",
- "interrobang": "⁉️",
- "iphone": "📱",
- "izakaya_lantern": "🏮",
- "jack_o_lantern": "🎃",
- "japan": "🗾",
- "japanese_castle": "🏯",
- "japanese_congratulations_button": "㊗️",
- "japanese_free_of_charge_button": "🈚",
- "japanese_goblin": "👺",
- "japanese_ogre": "👹",
- "japanese_reserved_button": "🈯",
- "japanese_secret_button": "㊙️",
- "japanese_service_charge_button": "🈂",
- "jeans": "👖",
- "joy": "😂",
- "joy_cat": "😹",
- "joystick": "🕹",
- "kaaba": "🕋",
- "kangaroo": "🦘",
- "key": "🔑",
- "keyboard": "⌨️",
- "keycap_ten": "🔟",
- "kick_scooter": "🛴",
- "kimono": "👘",
- "kiss": "💋",
- "kissing": "😗",
- "kissing_cat": "😽",
- "kissing_closed_eyes": "😚",
- "kissing_heart": "😘",
- "kissing_smiling_eyes": "😙",
- "kitchen_knife": "🔪",
- "kite": "🪁",
- "kiwi": "🥝",
- "kiwi_fruit": "🥝",
- "knife": "🔪",
- "koala": "🐨",
- "koko": "🈁",
- "lab_coat": "🥼",
- "label": "🏷",
- "lacrosse": "🥍",
- "large_blue_diamond": "🔷",
- "large_orange_diamond": "🔶",
- "last_quarter_moon": "🌗",
- "last_quarter_moon_with_face": "🌜",
- "last_track_button": "⏮️",
- "latin_cross": "✝",
- "laughing": "😆",
- "leafy_green": "🥬",
- "leaves": "🍃",
- "ledger": "📒",
- "left_arrow": "⬅",
- "left_arrow_curving_right": "↪",
- "left_facing_fist": "🤛",
- "left_luggage": "🛅",
- "left_right_arrow": "↔",
- "leftfacing_fist": "🤛",
- "leftright_arrow": "↔️",
- "leftwards_arrow_with_hook": "↩️",
- "leg": "🦵",
- "lemon": "🍋",
- "leo": "♌",
- "leopard": "🐆",
- "level_slider": "🎚",
- "libra": "♎",
- "light_rail": "🚈",
- "light_skin_tone": "🏻",
- "link": "🔗",
- "linked_paperclips": "🖇",
- "lion_face": "🦁",
- "lips": "👄",
- "lipstick": "💄",
- "lizard": "🦎",
- "llama": "🦙",
- "lobster": "🦞",
- "lock": "🔒",
- "lock_with_ink_pen": "🔏",
- "lollipop": "🍭",
- "loop": "➿",
- "lotion_bottle": "🧴",
- "loud_sound": "🔊",
- "loudspeaker": "📢",
- "love_hotel": "🏩",
- "love_letter": "💌",
- "love_you_gesture": "🤟",
- "loveyou_gesture": "🤟",
- "low_brightness": "🔅",
- "luggage": "🧳",
- "lying_face": "🤥",
- "m": "Ⓜ️",
- "mag": "🔍",
- "mag_right": "🔎",
- "mage": "🧙",
- "magnet": "🧲",
- "mahjong": "🀄",
- "mailbox": "📫",
- "mailbox_closed": "📪",
- "mailbox_with_mail": "📬",
- "mailbox_with_no_mail": "📭",
- "male_sign": "♂",
- "man": "👨",
- "man_dancing": "🕺",
- "man_in_suit": "🕴",
- "man_in_tuxedo": "🤵",
- "man_with_chinese_cap": "👲",
- "man_with_gua_pi_mao": "👲",
- "man_with_turban": "👳",
- "mango": "🥭",
- "mans_shoe": "👞",
- "mantelpiece_clock": "🕰",
- "manual_wheelchair": "🦽",
- "maple_leaf": "🍁",
- "martial_arts_uniform": "🥋",
- "mask": "😷",
- "massage": "💆",
- "mate": "🧉",
- "meat_on_bone": "🍖",
- "mechanical_arm": "🦾",
- "mechanical_leg": "🦿",
- "medal": "🏅",
- "medical_symbol": "⚕",
- "medium_skin_tone": "🏽",
- "mediumdark_skin_tone": "🏾",
- "mediumlight_skin_tone": "🏼",
- "mega": "📣",
- "melon": "🍈",
- "memo": "📝",
- "menorah": "🕎",
- "mens": "🚹",
- "merperson": "🧜",
- "metal": "🤘",
- "metro": "🚇",
- "microbe": "🦠",
- "microphone": "🎤",
- "microscope": "🔬",
- "middle_finger": "🖕",
- "military_medal": "🎖",
- "milk": "🥛",
- "milky_way": "🌌",
- "minibus": "🚐",
- "minidisc": "💽",
- "mobile_phone_off": "📴",
- "money_mouth": "🤑",
- "money_with_wings": "💸",
- "moneybag": "💰",
- "moneymouth_face": "🤑",
- "monkey": "🐒",
- "monkey_face": "🐵",
- "monorail": "🚝",
- "moon_cake": "🥮",
- "mortar_board": "🎓",
- "mosque": "🕌",
- "mosquito": "🦟",
- "motor_boat": "🛥",
- "motor_scooter": "🛵",
- "motorcycle": "🏍",
- "motorized_wheelchair": "🦼",
- "motorway": "🛣",
- "mount_fuji": "🗻",
- "mountain": "⛰️",
- "mountain_bicyclist": "🚵",
- "mountain_cableway": "🚠",
- "mountain_railway": "🚞",
- "mouse": "🐭",
- "mouse2": "🐁",
- "movie_camera": "🎥",
- "moyai": "🗿",
- "mrs_claus": "🤶",
- "multiplication_sign": "✖",
- "muscle": "💪",
- "mushroom": "🍄",
- "musical_keyboard": "🎹",
- "musical_note": "🎵",
- "musical_score": "🎼",
- "mute": "🔇",
- "nail_care": "💅",
- "name_badge": "📛",
- "national_park": "🏞",
- "nauseated_face": "🤢",
- "nazar_amulet": "🧿",
- "necktie": "👔",
- "negative_squared_cross_mark": "❎",
- "nerd": "🤓",
- "neutral_face": "😐",
- "new": "🆕",
- "new_moon": "🌑",
- "new_moon_with_face": "🌚",
- "newspaper": "📰",
- "next_track_button": "⏭️",
- "ng": "🆖",
- "night_with_stars": "🌃",
- "nine": "9⃣",
- "no_bell": "🔕",
- "no_bicycles": "🚳",
- "no_entry": "⛔",
- "no_entry_sign": "🚫",
- "no_good": "🙅",
- "no_mobile_phones": "📵",
- "no_mouth": "😶",
- "no_pedestrians": "🚷",
- "no_smoking": "🚭",
- "non-potable_water": "🚱",
- "nose": "👃",
- "notebook": "📓",
- "notebook_with_decorative_cover": "📔",
- "notes": "🎶",
- "nut_and_bolt": "🔩",
- "o": "⭕",
- "o_button_blood_type": "🅾",
- "ocean": "🌊",
- "octagonal_sign": "🛑",
- "octopus": "🐙",
- "oden": "🍢",
- "office": "🏢",
- "oil_drum": "🛢",
- "ok": "🆗",
- "ok_hand": "👌",
- "ok_woman": "🙆",
- "old_key": "🗝",
- "older_adult": "🧓",
- "older_man": "👴",
- "older_person": "🧓",
- "older_woman": "👵",
- "om_symbol": "🕉",
- "on": "🔛",
- "oncoming_automobile": "🚘",
- "oncoming_bus": "🚍",
- "oncoming_fist": "👊",
- "oncoming_police_car": "🚔",
- "oncoming_taxi": "🚖",
- "one": "1⃣",
- "onepiece_swimsuit": "🩱",
- "onion": "🧅",
- "open_file_folder": "📂",
- "open_hands": "👐",
- "open_mouth": "😮",
- "ophiuchus": "⛎",
- "orange_book": "📙",
- "orange_circle": "🟠",
- "orange_heart": "🧡",
- "orange_square": "🟧",
- "orangutan": "🦧",
- "orthodox_cross": "☦️",
- "otter": "🦦",
- "outbox_tray": "📤",
- "owl": "🦉",
- "ox": "🐂",
- "oyster": "🦪",
- "p_button": "🅿",
- "package": "📦",
- "page_facing_up": "📄",
- "page_with_curl": "📃",
- "pager": "📟",
- "paintbrush": "🖌",
- "palm_tree": "🌴",
- "palms_up_together": "🤲",
- "pancakes": "🥞",
- "panda_face": "🐼",
- "paperclip": "📎",
- "parachute": "🪂",
- "parrot": "🦜",
- "part_alternation_mark": "〽",
- "partly_sunny": "⛅",
- "partying_face": "🥳",
- "passenger_ship": "🛳",
- "passport_control": "🛂",
- "pause_button": "⏸️",
- "peace": "☮",
- "peace_symbol": "☮️",
- "peach": "🍑",
- "peacock": "🦚",
- "peanuts": "🥜",
- "pear": "🍐",
- "pen": "🖊",
- "pencil": "📝",
- "pencil2": "✏",
- "penguin": "🐧",
- "pensive": "😔",
- "people_with_bunny_ears_partying": "👯",
- "people_wrestling": "🤼",
- "performing_arts": "🎭",
- "persevere": "😣",
- "person": "🧑",
- "person_biking": "🚴",
- "person_bouncing_ball": "⛹️",
- "person_bowing": "🙇",
- "person_cartwheeling": "🤸",
- "person_climbing": "🧗",
- "person_doing_cartwheel": "🤸",
- "person_facepalming": "🤦",
- "person_fencing": "🤺",
- "person_frowning": "🙍",
- "person_gesturing_no": "🙅",
- "person_gesturing_ok": "🙆",
- "person_getting_haircut": "💇",
- "person_getting_massage": "💆",
- "person_in_lotus_position": "🧘",
- "person_in_steamy_room": "🧖",
- "person_juggling": "🤹",
- "person_kneeling": "🧎",
- "person_mountain_biking": "🚵",
- "person_playing_handball": "🤾",
- "person_playing_water_polo": "🤽",
- "person_pouting": "🙎",
- "person_raising_hand": "🙋",
- "person_rowing_boat": "🚣",
- "person_running": "🏃",
- "person_shrugging": "🤷",
- "person_standing": "🧍",
- "person_surfing": "🏄",
- "person_swimming": "🏊",
- "person_tipping_hand": "💁",
- "person_walking": "🚶",
- "person_wearing_turban": "👳",
- "person_with_blond_hair": "👱",
- "person_with_pouting_face": "🙎",
- "petri_dish": "🧫",
- "pick": "⛏️",
- "pie": "🥧",
- "pig": "🐷",
- "pig2": "🐖",
- "pig_nose": "🐽",
- "pill": "💊",
- "pinching_hand": "🤏",
- "pineapple": "🍍",
- "ping_pong": "🏓",
- "pisces": "♓",
- "pizza": "🍕",
- "place_of_worship": "🛐",
- "play_button": "▶",
- "play_or_pause_button": "⏯️",
- "play_pause": "⏯",
- "pleading_face": "🥺",
- "point_down": "👇",
- "point_left": "👈",
- "point_right": "👉",
- "point_up": "☝️",
- "point_up_2": "👆",
- "police_car": "🚓",
- "police_officer": "👮",
- "poodle": "🐩",
- "poop": "💩",
- "popcorn": "🍿",
- "post_office": "🏣",
- "postal_horn": "📯",
- "postbox": "📮",
- "potable_water": "🚰",
- "potato": "🥔",
- "pouch": "👝",
- "poultry_leg": "🍗",
- "pound": "💷",
- "pouting_cat": "😾",
- "pray": "🙏",
- "prayer_beads": "📿",
- "pregnant_woman": "🤰",
- "pretzel": "🥨",
- "prince": "🤴",
- "princess": "👸",
- "printer": "🖨",
- "probing_cane": "🦯",
- "punch": "👊",
- "purple_circle": "🟣",
- "purple_heart": "💜",
- "purse": "👛",
- "pushpin": "📌",
- "put_litter_in_its_place": "🚮",
- "puzzle_piece": "🧩",
- "question": "❓",
- "rabbit": "🐰",
- "rabbit2": "🐇",
- "raccoon": "🦝",
- "racehorse": "🐎",
- "racing_car": "🏎",
- "radio": "📻",
- "radio_button": "🔘",
- "radioactive": "☢️",
- "rage": "😡",
- "railway_car": "🚃",
- "railway_track": "🛤",
- "rainbow": "🌈",
- "raised_back_of_hand": "🤚",
- "raised_hand": "✋",
- "raised_hands": "🙌",
- "raising_hand": "🙋",
- "ram": "🐏",
- "ramen": "🍜",
- "rat": "🐀",
- "razor": "🪒",
- "receipt": "🧾",
- "record_button": "⏺️",
- "recycle": "♻",
- "recycling_symbol": "♻️",
- "red_car": "🚗",
- "red_circle": "🔴",
- "red_envelope": "🧧",
- "red_hair": "🦰",
- "red_heart": "❤",
- "red_square": "🟥",
- "regional_indicator_a": "🇦",
- "regional_indicator_b": "🇧",
- "regional_indicator_c": "🇨",
- "regional_indicator_d": "🇩",
- "regional_indicator_e": "🇪",
- "regional_indicator_f": "🇫",
- "regional_indicator_g": "🇬",
- "regional_indicator_h": "🇭",
- "regional_indicator_i": "🇮",
- "regional_indicator_j": "🇯",
- "regional_indicator_k": "🇰",
- "regional_indicator_l": "🇱",
- "regional_indicator_m": "🇲",
- "regional_indicator_n": "🇳",
- "regional_indicator_o": "🇴",
- "regional_indicator_p": "🇵",
- "regional_indicator_q": "🇶",
- "regional_indicator_r": "🇷",
- "regional_indicator_s": "🇸",
- "regional_indicator_t": "🇹",
- "regional_indicator_u": "🇺",
- "regional_indicator_v": "🇻",
- "regional_indicator_w": "🇼",
- "regional_indicator_x": "🇽",
- "regional_indicator_y": "🇾",
- "regional_indicator_z": "🇿",
- "registered": "®",
- "relieved": "😌",
- "reminder_ribbon": "🎗",
- "repeat": "🔁",
- "repeat_one": "🔂",
- "rescue_worker’s_helmet": "⛑️",
- "restroom": "🚻",
- "reverse_button": "◀",
- "revolving_hearts": "💞",
- "rewind": "⏪",
- "rhino": "🦏",
- "rhinoceros": "🦏",
- "ribbon": "🎀",
- "rice": "🍚",
- "rice_ball": "🍙",
- "rice_cracker": "🍘",
- "rice_scene": "🎑",
- "right_arrow": "➡️",
- "right_arrow_curving_down": "⤵",
- "right_arrow_curving_left": "↩",
- "right_arrow_curving_up": "⤴",
- "right_facing_fist": "🤜",
- "rightfacing_fist": "🤜",
- "ring": "💍",
- "ringed_planet": "🪐",
- "robot": "🤖",
- "rocket": "🚀",
- "rofl": "🤣",
- "roll_of_paper": "🧻",
- "rolledup_newspaper": "🗞",
- "roller_coaster": "🎢",
- "rolling_eyes": "🙄",
- "rolling_on_the_floor_laughing": "🤣",
- "rooster": "🐓",
- "rose": "🌹",
- "rosette": "🏵",
- "rotating_light": "🚨",
- "round_pushpin": "📍",
- "rowboat": "🚣",
- "rugby_football": "🏉",
- "runner": "🏃",
- "running_shirt_with_sash": "🎽",
- "safety_pin": "🧷",
- "safety_vest": "🦺",
- "sagittarius": "♐",
- "sailboat": "⛵",
- "sake": "🍶",
- "salad": "🥗",
- "salt": "🧂",
- "sandal": "👡",
- "sandwich": "🥪",
- "santa": "🎅",
- "sari": "🥻",
- "satellite": "📡",
- "sauropod": "🦕",
- "saxophone": "🎷",
- "scales": "⚖",
- "scarf": "🧣",
- "school": "🏫",
- "school_satchel": "🎒",
- "scissors": "✂",
- "scooter": "🛴",
- "scorpion": "🦂",
- "scorpius": "♏",
- "scream": "😱",
- "scream_cat": "🙀",
- "scroll": "📜",
- "seat": "💺",
- "second_place": "🥈",
- "secret": "㊙",
- "see_no_evil": "🙈",
- "seedling": "🌱",
- "selfie": "🤳",
- "seven": "7⃣",
- "shallow_pan_of_food": "🥘",
- "shamrock": "☘️",
- "shark": "🦈",
- "shaved_ice": "🍧",
- "sheep": "🐑",
- "shell": "🐚",
- "shield": "🛡",
- "shinto_shrine": "⛩️",
- "ship": "🚢",
- "shirt": "👕",
- "shopping_bags": "🛍",
- "shopping_cart": "🛒",
- "shorts": "🩳",
- "shower": "🚿",
- "shrimp": "🦐",
- "shushing_face": "🤫",
- "sign_of_the_horns": "🤘",
- "signal_strength": "📶",
- "six": "6⃣",
- "six_pointed_star": "🔯",
- "skateboard": "🛹",
- "ski": "🎿",
- "skier": "⛷️",
- "skull": "💀",
- "skull_and_crossbones": "☠️",
- "skull_crossbones": "☠",
- "skunk": "🦨",
- "sled": "🛷",
- "sleeping": "😴",
- "sleeping_accommodation": "🛌",
- "sleepy": "😪",
- "slight_frown": "🙁",
- "slight_smile": "🙂",
- "slightly_frowning_face": "🙁",
- "slot_machine": "🎰",
- "sloth": "🦥",
- "small_airplane": "🛩",
- "small_blue_diamond": "🔹",
- "small_orange_diamond": "🔸",
- "small_red_triangle": "🔺",
- "small_red_triangle_down": "🔻",
- "smile": "😄",
- "smile_cat": "😸",
- "smiley": "😃",
- "smiley_cat": "😺",
- "smiling": "☺️",
- "smiling_face": "☺",
- "smiling_face_with_hearts": "🥰",
- "smiling_imp": "😈",
- "smirk": "😏",
- "smirk_cat": "😼",
- "smoking": "🚬",
- "snail": "🐌",
- "snake": "🐍",
- "sneezing_face": "🤧",
- "snowboarder": "🏂",
- "snowcapped_mountain": "🏔",
- "snowflake": "❄",
- "snowman": "⛄",
- "soap": "🧼",
- "sob": "😭",
- "soccer": "⚽",
- "socks": "🧦",
- "softball": "🥎",
- "soon": "🔜",
- "sos": "🆘",
- "sound": "🔉",
- "space_invader": "👾",
- "spade_suit": "♠️",
- "spades": "♠",
- "spaghetti": "🍝",
- "sparkle": "❇",
- "sparkler": "🎇",
- "sparkles": "✨",
- "sparkling_heart": "💖",
- "speak_no_evil": "🙊",
- "speaker": "🔈",
- "speaking_head": "🗣",
- "speech_balloon": "💬",
- "speech_left": "🗨",
- "speedboat": "🚤",
- "spider": "🕷",
- "spider_web": "🕸",
- "spiral_calendar": "🗓",
- "spiral_notepad": "🗒",
- "sponge": "🧽",
- "spoon": "🥄",
- "squid": "🦑",
- "stadium": "🏟",
- "star": "⭐",
- "star2": "🌟",
- "star_and_crescent": "☪️",
- "star_of_david": "✡",
- "star_struck": "🤩",
- "stars": "🌠",
- "starstruck": "🤩",
- "station": "🚉",
- "statue_of_liberty": "🗽",
- "steam_locomotive": "🚂",
- "stethoscope": "🩺",
- "stew": "🍲",
- "stop_button": "⏹️",
- "stopwatch": "⏱️",
- "straight_ruler": "📏",
- "strawberry": "🍓",
- "stuck_out_tongue": "😛",
- "stuck_out_tongue_closed_eyes": "😝",
- "stuck_out_tongue_winking_eye": "😜",
- "studio_microphone": "🎙",
- "stuffed_flatbread": "🥙",
- "sun": "☀",
- "sun_behind_large_cloud": "🌥",
- "sun_behind_rain_cloud": "🌦",
- "sun_behind_small_cloud": "🌤",
- "sun_with_face": "🌞",
- "sunflower": "🌻",
- "sunglasses": "😎",
- "sunny": "☀️",
- "sunrise": "🌅",
- "sunrise_over_mountains": "🌄",
- "superhero": "🦸",
- "supervillain": "🦹",
- "surfer": "🏄",
- "sushi": "🍣",
- "suspension_railway": "🚟",
- "swan": "🦢",
- "sweat": "😓",
- "sweat_drops": "💦",
- "sweat_smile": "😅",
- "sweet_potato": "🍠",
- "swimmer": "🏊",
- "symbols": "🔣",
- "synagogue": "🕍",
- "syringe": "💉",
- "t_rex": "🦖",
- "taco": "🌮",
- "tada": "🎉",
- "takeout_box": "🥡",
- "tanabata_tree": "🎋",
- "tangerine": "🍊",
- "taurus": "♉",
- "taxi": "🚕",
- "tea": "🍵",
- "teddy_bear": "🧸",
- "telephone": "☎",
- "telephone_receiver": "📞",
- "telescope": "🔭",
- "tennis": "🎾",
- "tent": "⛺",
- "test_tube": "🧪",
- "thermometer": "🌡",
- "thermometer_face": "🤒",
- "thinking": "🤔",
- "third_place": "🥉",
- "thought_balloon": "💭",
- "thread": "🧵",
- "three": "3⃣",
- "thumbsdown": "👎",
- "thumbsup": "👍",
- "ticket": "🎫",
- "tiger": "🐯",
- "tiger2": "🐅",
- "timer_clock": "⏲️",
- "tired_face": "😫",
- "tm": "™",
- "toilet": "🚽",
- "tokyo_tower": "🗼",
- "tomato": "🍅",
- "tone1": "🏻",
- "tone2": "🏼",
- "tone3": "🏽",
- "tone4": "🏾",
- "tone5": "🏿",
- "tongue": "👅",
- "toolbox": "🧰",
- "tooth": "🦷",
- "top": "🔝",
- "tophat": "🎩",
- "tornado": "🌪",
- "track_next": "⏭",
- "track_previous": "⏮",
- "trackball": "🖲",
- "tractor": "🚜",
- "trade_mark": "™️",
- "traffic_light": "🚥",
- "train": "🚋",
- "train2": "🚆",
- "tram": "🚊",
- "trex": "🦖",
- "triangular_flag_on_post": "🚩",
- "triangular_ruler": "📐",
- "trident": "🔱",
- "triumph": "😤",
- "trolleybus": "🚎",
- "trophy": "🏆",
- "tropical_drink": "🍹",
- "tropical_fish": "🐠",
- "truck": "🚚",
- "trumpet": "🎺",
- "tulip": "🌷",
- "tumbler_glass": "🥃",
- "turkey": "🦃",
- "turtle": "🐢",
- "tv": "📺",
- "twisted_rightwards_arrows": "🔀",
- "two": "2⃣",
- "two_hearts": "💕",
- "two_men_holding_hands": "👬",
- "two_women_holding_hands": "👭",
- "u5272": "🈹",
- "u5408": "🈴",
- "u55b6": "🈺",
- "u6307": "🈯",
- "u6708": "🈷",
- "u6709": "🈶",
- "u6e80": "🈵",
- "u7121": "🈚",
- "u7533": "🈸",
- "u7981": "🈲",
- "u7a7a": "🈳",
- "umbrella": "☔",
- "umbrella_on_ground": "⛱️",
- "unamused": "😒",
- "underage": "🔞",
- "unicorn": "🦄",
- "unlock": "🔓",
- "up": "🆙",
- "up_arrow": "⬆",
- "updown_arrow": "↕️",
- "upleft_arrow": "↖️",
- "upright_arrow": "↗",
- "upside_down": "🙃",
- "v": "✌️",
- "vampire": "🧛",
- "vertical_traffic_light": "🚦",
- "vhs": "📼",
- "vibration_mode": "📳",
- "victory_hand": "✌",
- "video_camera": "📹",
- "video_game": "🎮",
- "violin": "🎻",
- "virgo": "♍",
- "volcano": "🌋",
- "volleyball": "🏐",
- "vs": "🆚",
- "vulcan": "🖖",
- "vulcan_salute": "🖖",
- "waffle": "🧇",
- "walking": "🚶",
- "waning_crescent_moon": "🌘",
- "waning_gibbous_moon": "🌖",
- "warning": "⚠",
- "wastebasket": "🗑",
- "watch": "⌚",
- "water_buffalo": "🐃",
- "watermelon": "🍉",
- "wave": "👋",
- "wavy_dash": "〰️",
- "waxing_crescent_moon": "🌒",
- "waxing_gibbous_moon": "🌔",
- "wc": "🚾",
- "weary": "😩",
- "wedding": "💒",
- "weightlifter": "🏋",
- "whale": "🐳",
- "whale2": "🐋",
- "wheel_of_dharma": "☸️",
- "wheelchair": "♿",
- "white_check_mark": "✅",
- "white_circle": "⚪",
- "white_flower": "💮",
- "white_hair": "🦳",
- "white_heart": "🤍",
- "white_large_square": "⬜",
- "white_medium_small_square": "◽",
- "white_medium_square": "◻️",
- "white_small_square": "▫️",
- "white_square_button": "🔳",
- "wilted_flower": "🥀",
- "wilted_rose": "🥀",
- "wind_blowing_face": "🌬",
- "wind_chime": "🎐",
- "wine_glass": "🍷",
- "wink": "😉",
- "wolf": "🐺",
- "woman": "👩",
- "woman_with_headscarf": "🧕",
- "womans_clothes": "👚",
- "womans_hat": "👒",
- "womens": "🚺",
- "woozy_face": "🥴",
- "world_map": "🗺",
- "worried": "😟",
- "wrench": "🔧",
- "writing_hand": "✍️",
- "x": "❌",
- "yarn": "🧶",
- "yawning_face": "🥱",
- "yellow_circle": "🟡",
- "yellow_heart": "💛",
- "yellow_square": "🟨",
- "yen": "💴",
- "yin_yang": "☯️",
- "yoyo": "🪀",
- "yum": "😋",
- "zany_face": "🤪",
- "zap": "⚡",
- "zebra": "🦓",
- "zero": "0⃣",
- "zipper_mouth": "🤐",
- "zombie": "🧟",
- "zzz": "💤"
-}
\ No newline at end of file
+{"smileys-and-emotion":[{"emoji":"😀","skin_tone_support":false,"name":"grinning face","slug":"grinning_face"},{"emoji":"😃","skin_tone_support":false,"name":"grinning face with big eyes","slug":"grinning_face_with_big_eyes"},{"emoji":"😄","skin_tone_support":false,"name":"grinning face with smiling eyes","slug":"grinning_face_with_smiling_eyes"},{"emoji":"😁","skin_tone_support":false,"name":"beaming face with smiling eyes","slug":"beaming_face_with_smiling_eyes"},{"emoji":"😆","skin_tone_support":false,"name":"grinning squinting face","slug":"grinning_squinting_face"},{"emoji":"😅","skin_tone_support":false,"name":"grinning face with sweat","slug":"grinning_face_with_sweat"},{"emoji":"🤣","skin_tone_support":false,"name":"rolling on the floor laughing","slug":"rolling_on_the_floor_laughing"},{"emoji":"😂","skin_tone_support":false,"name":"face with tears of joy","slug":"face_with_tears_of_joy"},{"emoji":"🙂","skin_tone_support":false,"name":"slightly smiling face","slug":"slightly_smiling_face"},{"emoji":"🙃","skin_tone_support":false,"name":"upside-down face","slug":"upside_down_face"},{"emoji":"🫠","skin_tone_support":false,"name":"melting face","slug":"melting_face"},{"emoji":"😉","skin_tone_support":false,"name":"winking face","slug":"winking_face"},{"emoji":"😊","skin_tone_support":false,"name":"smiling face with smiling eyes","slug":"smiling_face_with_smiling_eyes"},{"emoji":"😇","skin_tone_support":false,"name":"smiling face with halo","slug":"smiling_face_with_halo"},{"emoji":"🥰","skin_tone_support":false,"name":"smiling face with hearts","slug":"smiling_face_with_hearts"},{"emoji":"😍","skin_tone_support":false,"name":"smiling face with heart-eyes","slug":"smiling_face_with_heart_eyes"},{"emoji":"🤩","skin_tone_support":false,"name":"star-struck","slug":"star_struck"},{"emoji":"😘","skin_tone_support":false,"name":"face blowing a kiss","slug":"face_blowing_a_kiss"},{"emoji":"😗","skin_tone_support":false,"name":"kissing face","slug":"kissing_face"},{"emoji":"☺️","skin_tone_support":false,"name":"smiling face","slug":"smiling_face"},{"emoji":"😚","skin_tone_support":false,"name":"kissing face with closed eyes","slug":"kissing_face_with_closed_eyes"},{"emoji":"😙","skin_tone_support":false,"name":"kissing face with smiling eyes","slug":"kissing_face_with_smiling_eyes"},{"emoji":"🥲","skin_tone_support":false,"name":"smiling face with tear","slug":"smiling_face_with_tear"},{"emoji":"😋","skin_tone_support":false,"name":"face savoring food","slug":"face_savoring_food"},{"emoji":"😛","skin_tone_support":false,"name":"face with tongue","slug":"face_with_tongue"},{"emoji":"😜","skin_tone_support":false,"name":"winking face with tongue","slug":"winking_face_with_tongue"},{"emoji":"🤪","skin_tone_support":false,"name":"zany face","slug":"zany_face"},{"emoji":"😝","skin_tone_support":false,"name":"squinting face with tongue","slug":"squinting_face_with_tongue"},{"emoji":"🤑","skin_tone_support":false,"name":"money-mouth face","slug":"money_mouth_face"},{"emoji":"🤗","skin_tone_support":false,"name":"smiling face with open hands","slug":"smiling_face_with_open_hands"},{"emoji":"🤭","skin_tone_support":false,"name":"face with hand over mouth","slug":"face_with_hand_over_mouth"},{"emoji":"🫢","skin_tone_support":false,"name":"face with open eyes and hand over mouth","slug":"face_with_open_eyes_and_hand_over_mouth"},{"emoji":"🫣","skin_tone_support":false,"name":"face with peeking eye","slug":"face_with_peeking_eye"},{"emoji":"🤫","skin_tone_support":false,"name":"shushing face","slug":"shushing_face"},{"emoji":"🤔","skin_tone_support":false,"name":"thinking face","slug":"thinking_face"},{"emoji":"🫡","skin_tone_support":false,"name":"saluting face","slug":"saluting_face"},{"emoji":"🤐","skin_tone_support":false,"name":"zipper-mouth face","slug":"zipper_mouth_face"},{"emoji":"🤨","skin_tone_support":false,"name":"face with raised eyebrow","slug":"face_with_raised_eyebrow"},{"emoji":"😐","skin_tone_support":false,"name":"neutral face","slug":"neutral_face"},{"emoji":"😑","skin_tone_support":false,"name":"expressionless face","slug":"expressionless_face"},{"emoji":"😶","skin_tone_support":false,"name":"face without mouth","slug":"face_without_mouth"},{"emoji":"🫥","skin_tone_support":false,"name":"dotted line face","slug":"dotted_line_face"},{"emoji":"😶🌫️","skin_tone_support":false,"name":"face in clouds","slug":"face_in_clouds"},{"emoji":"😏","skin_tone_support":false,"name":"smirking face","slug":"smirking_face"},{"emoji":"😒","skin_tone_support":false,"name":"unamused face","slug":"unamused_face"},{"emoji":"🙄","skin_tone_support":false,"name":"face with rolling eyes","slug":"face_with_rolling_eyes"},{"emoji":"😬","skin_tone_support":false,"name":"grimacing face","slug":"grimacing_face"},{"emoji":"😮💨","skin_tone_support":false,"name":"face exhaling","slug":"face_exhaling"},{"emoji":"🤥","skin_tone_support":false,"name":"lying face","slug":"lying_face"},{"emoji":"😌","skin_tone_support":false,"name":"relieved face","slug":"relieved_face"},{"emoji":"😔","skin_tone_support":false,"name":"pensive face","slug":"pensive_face"},{"emoji":"😪","skin_tone_support":false,"name":"sleepy face","slug":"sleepy_face"},{"emoji":"🤤","skin_tone_support":false,"name":"drooling face","slug":"drooling_face"},{"emoji":"😴","skin_tone_support":false,"name":"sleeping face","slug":"sleeping_face"},{"emoji":"😷","skin_tone_support":false,"name":"face with medical mask","slug":"face_with_medical_mask"},{"emoji":"🤒","skin_tone_support":false,"name":"face with thermometer","slug":"face_with_thermometer"},{"emoji":"🤕","skin_tone_support":false,"name":"face with head-bandage","slug":"face_with_head_bandage"},{"emoji":"🤢","skin_tone_support":false,"name":"nauseated face","slug":"nauseated_face"},{"emoji":"🤮","skin_tone_support":false,"name":"face vomiting","slug":"face_vomiting"},{"emoji":"🤧","skin_tone_support":false,"name":"sneezing face","slug":"sneezing_face"},{"emoji":"🥵","skin_tone_support":false,"name":"hot face","slug":"hot_face"},{"emoji":"🥶","skin_tone_support":false,"name":"cold face","slug":"cold_face"},{"emoji":"🥴","skin_tone_support":false,"name":"woozy face","slug":"woozy_face"},{"emoji":"😵","skin_tone_support":false,"name":"face with crossed-out eyes","slug":"face_with_crossed_out_eyes"},{"emoji":"😵💫","skin_tone_support":false,"name":"face with spiral eyes","slug":"face_with_spiral_eyes"},{"emoji":"🤯","skin_tone_support":false,"name":"exploding head","slug":"exploding_head"},{"emoji":"🤠","skin_tone_support":false,"name":"cowboy hat face","slug":"cowboy_hat_face"},{"emoji":"🥳","skin_tone_support":false,"name":"partying face","slug":"partying_face"},{"emoji":"🥸","skin_tone_support":false,"name":"disguised face","slug":"disguised_face"},{"emoji":"😎","skin_tone_support":false,"name":"smiling face with sunglasses","slug":"smiling_face_with_sunglasses"},{"emoji":"🤓","skin_tone_support":false,"name":"nerd face","slug":"nerd_face"},{"emoji":"🧐","skin_tone_support":false,"name":"face with monocle","slug":"face_with_monocle"},{"emoji":"😕","skin_tone_support":false,"name":"confused face","slug":"confused_face"},{"emoji":"🫤","skin_tone_support":false,"name":"face with diagonal mouth","slug":"face_with_diagonal_mouth"},{"emoji":"😟","skin_tone_support":false,"name":"worried face","slug":"worried_face"},{"emoji":"🙁","skin_tone_support":false,"name":"slightly frowning face","slug":"slightly_frowning_face"},{"emoji":"☹️","skin_tone_support":false,"name":"frowning face","slug":"frowning_face"},{"emoji":"😮","skin_tone_support":false,"name":"face with open mouth","slug":"face_with_open_mouth"},{"emoji":"😯","skin_tone_support":false,"name":"hushed face","slug":"hushed_face"},{"emoji":"😲","skin_tone_support":false,"name":"astonished face","slug":"astonished_face"},{"emoji":"😳","skin_tone_support":false,"name":"flushed face","slug":"flushed_face"},{"emoji":"🥺","skin_tone_support":false,"name":"pleading face","slug":"pleading_face"},{"emoji":"🥹","skin_tone_support":false,"name":"face holding back tears","slug":"face_holding_back_tears"},{"emoji":"😦","skin_tone_support":false,"name":"frowning face with open mouth","slug":"frowning_face_with_open_mouth"},{"emoji":"😧","skin_tone_support":false,"name":"anguished face","slug":"anguished_face"},{"emoji":"😨","skin_tone_support":false,"name":"fearful face","slug":"fearful_face"},{"emoji":"😰","skin_tone_support":false,"name":"anxious face with sweat","slug":"anxious_face_with_sweat"},{"emoji":"😥","skin_tone_support":false,"name":"sad but relieved face","slug":"sad_but_relieved_face"},{"emoji":"😢","skin_tone_support":false,"name":"crying face","slug":"crying_face"},{"emoji":"😭","skin_tone_support":false,"name":"loudly crying face","slug":"loudly_crying_face"},{"emoji":"😱","skin_tone_support":false,"name":"face screaming in fear","slug":"face_screaming_in_fear"},{"emoji":"😖","skin_tone_support":false,"name":"confounded face","slug":"confounded_face"},{"emoji":"😣","skin_tone_support":false,"name":"persevering face","slug":"persevering_face"},{"emoji":"😞","skin_tone_support":false,"name":"disappointed face","slug":"disappointed_face"},{"emoji":"😓","skin_tone_support":false,"name":"downcast face with sweat","slug":"downcast_face_with_sweat"},{"emoji":"😩","skin_tone_support":false,"name":"weary face","slug":"weary_face"},{"emoji":"😫","skin_tone_support":false,"name":"tired face","slug":"tired_face"},{"emoji":"🥱","skin_tone_support":false,"name":"yawning face","slug":"yawning_face"},{"emoji":"😤","skin_tone_support":false,"name":"face with steam from nose","slug":"face_with_steam_from_nose"},{"emoji":"😡","skin_tone_support":false,"name":"pouting face","slug":"pouting_face"},{"emoji":"😠","skin_tone_support":false,"name":"angry face","slug":"angry_face"},{"emoji":"🤬","skin_tone_support":false,"name":"face with symbols on mouth","slug":"face_with_symbols_on_mouth"},{"emoji":"😈","skin_tone_support":false,"name":"smiling face with horns","slug":"smiling_face_with_horns"},{"emoji":"👿","skin_tone_support":false,"name":"angry face with horns","slug":"angry_face_with_horns"},{"emoji":"💀","skin_tone_support":false,"name":"skull","slug":"skull"},{"emoji":"☠️","skin_tone_support":false,"name":"skull and crossbones","slug":"skull_and_crossbones"},{"emoji":"💩","skin_tone_support":false,"name":"pile of poo","slug":"pile_of_poo"},{"emoji":"🤡","skin_tone_support":false,"name":"clown face","slug":"clown_face"},{"emoji":"👹","skin_tone_support":false,"name":"ogre","slug":"ogre"},{"emoji":"👺","skin_tone_support":false,"name":"goblin","slug":"goblin"},{"emoji":"👻","skin_tone_support":false,"name":"ghost","slug":"ghost"},{"emoji":"👽","skin_tone_support":false,"name":"alien","slug":"alien"},{"emoji":"👾","skin_tone_support":false,"name":"alien monster","slug":"alien_monster"},{"emoji":"🤖","skin_tone_support":false,"name":"robot","slug":"robot"},{"emoji":"😺","skin_tone_support":false,"name":"grinning cat","slug":"grinning_cat"},{"emoji":"😸","skin_tone_support":false,"name":"grinning cat with smiling eyes","slug":"grinning_cat_with_smiling_eyes"},{"emoji":"😹","skin_tone_support":false,"name":"cat with tears of joy","slug":"cat_with_tears_of_joy"},{"emoji":"😻","skin_tone_support":false,"name":"smiling cat with heart-eyes","slug":"smiling_cat_with_heart_eyes"},{"emoji":"😼","skin_tone_support":false,"name":"cat with wry smile","slug":"cat_with_wry_smile"},{"emoji":"😽","skin_tone_support":false,"name":"kissing cat","slug":"kissing_cat"},{"emoji":"🙀","skin_tone_support":false,"name":"weary cat","slug":"weary_cat"},{"emoji":"😿","skin_tone_support":false,"name":"crying cat","slug":"crying_cat"},{"emoji":"😾","skin_tone_support":false,"name":"pouting cat","slug":"pouting_cat"},{"emoji":"🙈","skin_tone_support":false,"name":"see-no-evil monkey","slug":"see_no_evil_monkey"},{"emoji":"🙉","skin_tone_support":false,"name":"hear-no-evil monkey","slug":"hear_no_evil_monkey"},{"emoji":"🙊","skin_tone_support":false,"name":"speak-no-evil monkey","slug":"speak_no_evil_monkey"},{"emoji":"💋","skin_tone_support":false,"name":"kiss mark","slug":"kiss_mark"},{"emoji":"💌","skin_tone_support":false,"name":"love letter","slug":"love_letter"},{"emoji":"💘","skin_tone_support":false,"name":"heart with arrow","slug":"heart_with_arrow"},{"emoji":"💝","skin_tone_support":false,"name":"heart with ribbon","slug":"heart_with_ribbon"},{"emoji":"💖","skin_tone_support":false,"name":"sparkling heart","slug":"sparkling_heart"},{"emoji":"💗","skin_tone_support":false,"name":"growing heart","slug":"growing_heart"},{"emoji":"💓","skin_tone_support":false,"name":"beating heart","slug":"beating_heart"},{"emoji":"💞","skin_tone_support":false,"name":"revolving hearts","slug":"revolving_hearts"},{"emoji":"💕","skin_tone_support":false,"name":"two hearts","slug":"two_hearts"},{"emoji":"💟","skin_tone_support":false,"name":"heart decoration","slug":"heart_decoration"},{"emoji":"❣️","skin_tone_support":false,"name":"heart exclamation","slug":"heart_exclamation"},{"emoji":"💔","skin_tone_support":false,"name":"broken heart","slug":"broken_heart"},{"emoji":"❤️🔥","skin_tone_support":false,"name":"heart on fire","slug":"heart_on_fire"},{"emoji":"❤️🩹","skin_tone_support":false,"name":"mending heart","slug":"mending_heart"},{"emoji":"❤️","skin_tone_support":false,"name":"red heart","slug":"red_heart"},{"emoji":"🧡","skin_tone_support":false,"name":"orange heart","slug":"orange_heart"},{"emoji":"💛","skin_tone_support":false,"name":"yellow heart","slug":"yellow_heart"},{"emoji":"💚","skin_tone_support":false,"name":"green heart","slug":"green_heart"},{"emoji":"💙","skin_tone_support":false,"name":"blue heart","slug":"blue_heart"},{"emoji":"💜","skin_tone_support":false,"name":"purple heart","slug":"purple_heart"},{"emoji":"🤎","skin_tone_support":false,"name":"brown heart","slug":"brown_heart"},{"emoji":"🖤","skin_tone_support":false,"name":"black heart","slug":"black_heart"},{"emoji":"🤍","skin_tone_support":false,"name":"white heart","slug":"white_heart"},{"emoji":"💯","skin_tone_support":false,"name":"hundred points","slug":"hundred_points"},{"emoji":"💢","skin_tone_support":false,"name":"anger symbol","slug":"anger_symbol"},{"emoji":"💥","skin_tone_support":false,"name":"collision","slug":"collision"},{"emoji":"💫","skin_tone_support":false,"name":"dizzy","slug":"dizzy"},{"emoji":"💦","skin_tone_support":false,"name":"sweat droplets","slug":"sweat_droplets"},{"emoji":"💨","skin_tone_support":false,"name":"dashing away","slug":"dashing_away"},{"emoji":"🕳️","skin_tone_support":false,"name":"hole","slug":"hole"},{"emoji":"💣","skin_tone_support":false,"name":"bomb","slug":"bomb"},{"emoji":"💬","skin_tone_support":false,"name":"speech balloon","slug":"speech_balloon"},{"emoji":"👁️🗨️","skin_tone_support":false,"name":"eye in speech bubble","slug":"eye_in_speech_bubble"},{"emoji":"🗨️","skin_tone_support":false,"name":"left speech bubble","slug":"left_speech_bubble"},{"emoji":"🗯️","skin_tone_support":false,"name":"right anger bubble","slug":"right_anger_bubble"},{"emoji":"💭","skin_tone_support":false,"name":"thought balloon","slug":"thought_balloon"},{"emoji":"💤","skin_tone_support":false,"name":"zzz","slug":"zzz"}],"people-and-body":[{"emoji":"👋","skin_tone_support":true,"name":"waving hand","slug":"waving_hand"},{"emoji":"🤚","skin_tone_support":true,"name":"raised back of hand","slug":"raised_back_of_hand"},{"emoji":"🖐️","skin_tone_support":true,"name":"hand with fingers splayed","slug":"hand_with_fingers_splayed"},{"emoji":"✋","skin_tone_support":true,"name":"raised hand","slug":"raised_hand"},{"emoji":"🖖","skin_tone_support":true,"name":"vulcan salute","slug":"vulcan_salute"},{"emoji":"🫱","skin_tone_support":true,"name":"rightwards hand","slug":"rightwards_hand"},{"emoji":"🫲","skin_tone_support":true,"name":"leftwards hand","slug":"leftwards_hand"},{"emoji":"🫳","skin_tone_support":true,"name":"palm down hand","slug":"palm_down_hand"},{"emoji":"🫴","skin_tone_support":true,"name":"palm up hand","slug":"palm_up_hand"},{"emoji":"👌","skin_tone_support":true,"name":"OK hand","slug":"ok_hand"},{"emoji":"🤌","skin_tone_support":true,"name":"pinched fingers","slug":"pinched_fingers"},{"emoji":"🤏","skin_tone_support":true,"name":"pinching hand","slug":"pinching_hand"},{"emoji":"✌️","skin_tone_support":true,"name":"victory hand","slug":"victory_hand"},{"emoji":"🤞","skin_tone_support":true,"name":"crossed fingers","slug":"crossed_fingers"},{"emoji":"🫰","skin_tone_support":true,"name":"hand with index finger and thumb crossed","slug":"hand_with_index_finger_and_thumb_crossed"},{"emoji":"🤟","skin_tone_support":true,"name":"love-you gesture","slug":"love_you_gesture"},{"emoji":"🤘","skin_tone_support":true,"name":"sign of the horns","slug":"sign_of_the_horns"},{"emoji":"🤙","skin_tone_support":true,"name":"call me hand","slug":"call_me_hand"},{"emoji":"👈","skin_tone_support":true,"name":"backhand index pointing left","slug":"backhand_index_pointing_left"},{"emoji":"👉","skin_tone_support":true,"name":"backhand index pointing right","slug":"backhand_index_pointing_right"},{"emoji":"👆","skin_tone_support":true,"name":"backhand index pointing up","slug":"backhand_index_pointing_up"},{"emoji":"🖕","skin_tone_support":true,"name":"middle finger","slug":"middle_finger"},{"emoji":"👇","skin_tone_support":true,"name":"backhand index pointing down","slug":"backhand_index_pointing_down"},{"emoji":"☝️","skin_tone_support":true,"name":"index pointing up","slug":"index_pointing_up"},{"emoji":"🫵","skin_tone_support":true,"name":"index pointing at the viewer","slug":"index_pointing_at_the_viewer"},{"emoji":"👍","skin_tone_support":true,"name":"thumbs up","slug":"thumbs_up"},{"emoji":"👎","skin_tone_support":true,"name":"thumbs down","slug":"thumbs_down"},{"emoji":"✊","skin_tone_support":true,"name":"raised fist","slug":"raised_fist"},{"emoji":"👊","skin_tone_support":true,"name":"oncoming fist","slug":"oncoming_fist"},{"emoji":"🤛","skin_tone_support":true,"name":"left-facing fist","slug":"left_facing_fist"},{"emoji":"🤜","skin_tone_support":true,"name":"right-facing fist","slug":"right_facing_fist"},{"emoji":"👏","skin_tone_support":true,"name":"clapping hands","slug":"clapping_hands"},{"emoji":"🙌","skin_tone_support":true,"name":"raising hands","slug":"raising_hands"},{"emoji":"🫶","skin_tone_support":true,"name":"heart hands","slug":"heart_hands"},{"emoji":"👐","skin_tone_support":true,"name":"open hands","slug":"open_hands"},{"emoji":"🤲","skin_tone_support":true,"name":"palms up together","slug":"palms_up_together"},{"emoji":"🤝","skin_tone_support":true,"name":"handshake","slug":"handshake"},{"emoji":"🙏","skin_tone_support":true,"name":"folded hands","slug":"folded_hands"},{"emoji":"✍️","skin_tone_support":true,"name":"writing hand","slug":"writing_hand"},{"emoji":"💅","skin_tone_support":true,"name":"nail polish","slug":"nail_polish"},{"emoji":"🤳","skin_tone_support":true,"name":"selfie","slug":"selfie"},{"emoji":"💪","skin_tone_support":true,"name":"flexed biceps","slug":"flexed_biceps"},{"emoji":"🦾","skin_tone_support":false,"name":"mechanical arm","slug":"mechanical_arm"},{"emoji":"🦿","skin_tone_support":false,"name":"mechanical leg","slug":"mechanical_leg"},{"emoji":"🦵","skin_tone_support":true,"name":"leg","slug":"leg"},{"emoji":"🦶","skin_tone_support":true,"name":"foot","slug":"foot"},{"emoji":"👂","skin_tone_support":true,"name":"ear","slug":"ear"},{"emoji":"🦻","skin_tone_support":true,"name":"ear with hearing aid","slug":"ear_with_hearing_aid"},{"emoji":"👃","skin_tone_support":true,"name":"nose","slug":"nose"},{"emoji":"🧠","skin_tone_support":false,"name":"brain","slug":"brain"},{"emoji":"🫀","skin_tone_support":false,"name":"anatomical heart","slug":"anatomical_heart"},{"emoji":"🫁","skin_tone_support":false,"name":"lungs","slug":"lungs"},{"emoji":"🦷","skin_tone_support":false,"name":"tooth","slug":"tooth"},{"emoji":"🦴","skin_tone_support":false,"name":"bone","slug":"bone"},{"emoji":"👀","skin_tone_support":false,"name":"eyes","slug":"eyes"},{"emoji":"👁️","skin_tone_support":false,"name":"eye","slug":"eye"},{"emoji":"👅","skin_tone_support":false,"name":"tongue","slug":"tongue"},{"emoji":"👄","skin_tone_support":false,"name":"mouth","slug":"mouth"},{"emoji":"🫦","skin_tone_support":false,"name":"biting lip","slug":"biting_lip"},{"emoji":"👶","skin_tone_support":true,"name":"baby","slug":"baby"},{"emoji":"🧒","skin_tone_support":true,"name":"child","slug":"child"},{"emoji":"👦","skin_tone_support":true,"name":"boy","slug":"boy"},{"emoji":"👧","skin_tone_support":true,"name":"girl","slug":"girl"},{"emoji":"🧑","skin_tone_support":true,"name":"person","slug":"person"},{"emoji":"👱","skin_tone_support":true,"name":"person blond hair","slug":"person_blond_hair"},{"emoji":"👨","skin_tone_support":true,"name":"man","slug":"man"},{"emoji":"🧔","skin_tone_support":true,"name":"person beard","slug":"person_beard"},{"emoji":"🧔♂️","skin_tone_support":true,"name":"man beard","slug":"man_beard"},{"emoji":"🧔♀️","skin_tone_support":true,"name":"woman beard","slug":"woman_beard"},{"emoji":"👨🦰","skin_tone_support":true,"name":"man red hair","slug":"man_red_hair"},{"emoji":"👨🦱","skin_tone_support":true,"name":"man curly hair","slug":"man_curly_hair"},{"emoji":"👨🦳","skin_tone_support":true,"name":"man white hair","slug":"man_white_hair"},{"emoji":"👨🦲","skin_tone_support":true,"name":"man bald","slug":"man_bald"},{"emoji":"👩","skin_tone_support":true,"name":"woman","slug":"woman"},{"emoji":"👩🦰","skin_tone_support":true,"name":"woman red hair","slug":"woman_red_hair"},{"emoji":"🧑🦰","skin_tone_support":true,"name":"person red hair","slug":"person_red_hair"},{"emoji":"👩🦱","skin_tone_support":true,"name":"woman curly hair","slug":"woman_curly_hair"},{"emoji":"🧑🦱","skin_tone_support":true,"name":"person curly hair","slug":"person_curly_hair"},{"emoji":"👩🦳","skin_tone_support":true,"name":"woman white hair","slug":"woman_white_hair"},{"emoji":"🧑🦳","skin_tone_support":true,"name":"person white hair","slug":"person_white_hair"},{"emoji":"👩🦲","skin_tone_support":true,"name":"woman bald","slug":"woman_bald"},{"emoji":"🧑🦲","skin_tone_support":true,"name":"person bald","slug":"person_bald"},{"emoji":"👱♀️","skin_tone_support":true,"name":"woman blond hair","slug":"woman_blond_hair"},{"emoji":"👱♂️","skin_tone_support":true,"name":"man blond hair","slug":"man_blond_hair"},{"emoji":"🧓","skin_tone_support":true,"name":"older person","slug":"older_person"},{"emoji":"👴","skin_tone_support":true,"name":"old man","slug":"old_man"},{"emoji":"👵","skin_tone_support":true,"name":"old woman","slug":"old_woman"},{"emoji":"🙍","skin_tone_support":true,"name":"person frowning","slug":"person_frowning"},{"emoji":"🙍♂️","skin_tone_support":true,"name":"man frowning","slug":"man_frowning"},{"emoji":"🙍♀️","skin_tone_support":true,"name":"woman frowning","slug":"woman_frowning"},{"emoji":"🙎","skin_tone_support":true,"name":"person pouting","slug":"person_pouting"},{"emoji":"🙎♂️","skin_tone_support":true,"name":"man pouting","slug":"man_pouting"},{"emoji":"🙎♀️","skin_tone_support":true,"name":"woman pouting","slug":"woman_pouting"},{"emoji":"🙅","skin_tone_support":true,"name":"person gesturing NO","slug":"person_gesturing_no"},{"emoji":"🙅♂️","skin_tone_support":true,"name":"man gesturing NO","slug":"man_gesturing_no"},{"emoji":"🙅♀️","skin_tone_support":true,"name":"woman gesturing NO","slug":"woman_gesturing_no"},{"emoji":"🙆","skin_tone_support":true,"name":"person gesturing OK","slug":"person_gesturing_ok"},{"emoji":"🙆♂️","skin_tone_support":true,"name":"man gesturing OK","slug":"man_gesturing_ok"},{"emoji":"🙆♀️","skin_tone_support":true,"name":"woman gesturing OK","slug":"woman_gesturing_ok"},{"emoji":"💁","skin_tone_support":true,"name":"person tipping hand","slug":"person_tipping_hand"},{"emoji":"💁♂️","skin_tone_support":true,"name":"man tipping hand","slug":"man_tipping_hand"},{"emoji":"💁♀️","skin_tone_support":true,"name":"woman tipping hand","slug":"woman_tipping_hand"},{"emoji":"🙋","skin_tone_support":true,"name":"person raising hand","slug":"person_raising_hand"},{"emoji":"🙋♂️","skin_tone_support":true,"name":"man raising hand","slug":"man_raising_hand"},{"emoji":"🙋♀️","skin_tone_support":true,"name":"woman raising hand","slug":"woman_raising_hand"},{"emoji":"🧏","skin_tone_support":true,"name":"deaf person","slug":"deaf_person"},{"emoji":"🧏♂️","skin_tone_support":true,"name":"deaf man","slug":"deaf_man"},{"emoji":"🧏♀️","skin_tone_support":true,"name":"deaf woman","slug":"deaf_woman"},{"emoji":"🙇","skin_tone_support":true,"name":"person bowing","slug":"person_bowing"},{"emoji":"🙇♂️","skin_tone_support":true,"name":"man bowing","slug":"man_bowing"},{"emoji":"🙇♀️","skin_tone_support":true,"name":"woman bowing","slug":"woman_bowing"},{"emoji":"🤦","skin_tone_support":true,"name":"person facepalming","slug":"person_facepalming"},{"emoji":"🤦♂️","skin_tone_support":true,"name":"man facepalming","slug":"man_facepalming"},{"emoji":"🤦♀️","skin_tone_support":true,"name":"woman facepalming","slug":"woman_facepalming"},{"emoji":"🤷","skin_tone_support":true,"name":"person shrugging","slug":"person_shrugging"},{"emoji":"🤷♂️","skin_tone_support":true,"name":"man shrugging","slug":"man_shrugging"},{"emoji":"🤷♀️","skin_tone_support":true,"name":"woman shrugging","slug":"woman_shrugging"},{"emoji":"🧑⚕️","skin_tone_support":true,"name":"health worker","slug":"health_worker"},{"emoji":"👨⚕️","skin_tone_support":true,"name":"man health worker","slug":"man_health_worker"},{"emoji":"👩⚕️","skin_tone_support":true,"name":"woman health worker","slug":"woman_health_worker"},{"emoji":"🧑🎓","skin_tone_support":true,"name":"student","slug":"student"},{"emoji":"👨🎓","skin_tone_support":true,"name":"man student","slug":"man_student"},{"emoji":"👩🎓","skin_tone_support":true,"name":"woman student","slug":"woman_student"},{"emoji":"🧑🏫","skin_tone_support":true,"name":"teacher","slug":"teacher"},{"emoji":"👨🏫","skin_tone_support":true,"name":"man teacher","slug":"man_teacher"},{"emoji":"👩🏫","skin_tone_support":true,"name":"woman teacher","slug":"woman_teacher"},{"emoji":"🧑⚖️","skin_tone_support":true,"name":"judge","slug":"judge"},{"emoji":"👨⚖️","skin_tone_support":true,"name":"man judge","slug":"man_judge"},{"emoji":"👩⚖️","skin_tone_support":true,"name":"woman judge","slug":"woman_judge"},{"emoji":"🧑🌾","skin_tone_support":true,"name":"farmer","slug":"farmer"},{"emoji":"👨🌾","skin_tone_support":true,"name":"man farmer","slug":"man_farmer"},{"emoji":"👩🌾","skin_tone_support":true,"name":"woman farmer","slug":"woman_farmer"},{"emoji":"🧑🍳","skin_tone_support":true,"name":"cook","slug":"cook"},{"emoji":"👨🍳","skin_tone_support":true,"name":"man cook","slug":"man_cook"},{"emoji":"👩🍳","skin_tone_support":true,"name":"woman cook","slug":"woman_cook"},{"emoji":"🧑🔧","skin_tone_support":true,"name":"mechanic","slug":"mechanic"},{"emoji":"👨🔧","skin_tone_support":true,"name":"man mechanic","slug":"man_mechanic"},{"emoji":"👩🔧","skin_tone_support":true,"name":"woman mechanic","slug":"woman_mechanic"},{"emoji":"🧑🏭","skin_tone_support":true,"name":"factory worker","slug":"factory_worker"},{"emoji":"👨🏭","skin_tone_support":true,"name":"man factory worker","slug":"man_factory_worker"},{"emoji":"👩🏭","skin_tone_support":true,"name":"woman factory worker","slug":"woman_factory_worker"},{"emoji":"🧑💼","skin_tone_support":true,"name":"office worker","slug":"office_worker"},{"emoji":"👨💼","skin_tone_support":true,"name":"man office worker","slug":"man_office_worker"},{"emoji":"👩💼","skin_tone_support":true,"name":"woman office worker","slug":"woman_office_worker"},{"emoji":"🧑🔬","skin_tone_support":true,"name":"scientist","slug":"scientist"},{"emoji":"👨🔬","skin_tone_support":true,"name":"man scientist","slug":"man_scientist"},{"emoji":"👩🔬","skin_tone_support":true,"name":"woman scientist","slug":"woman_scientist"},{"emoji":"🧑💻","skin_tone_support":true,"name":"technologist","slug":"technologist"},{"emoji":"👨💻","skin_tone_support":true,"name":"man technologist","slug":"man_technologist"},{"emoji":"👩💻","skin_tone_support":true,"name":"woman technologist","slug":"woman_technologist"},{"emoji":"🧑🎤","skin_tone_support":true,"name":"singer","slug":"singer"},{"emoji":"👨🎤","skin_tone_support":true,"name":"man singer","slug":"man_singer"},{"emoji":"👩🎤","skin_tone_support":true,"name":"woman singer","slug":"woman_singer"},{"emoji":"🧑🎨","skin_tone_support":true,"name":"artist","slug":"artist"},{"emoji":"👨🎨","skin_tone_support":true,"name":"man artist","slug":"man_artist"},{"emoji":"👩🎨","skin_tone_support":true,"name":"woman artist","slug":"woman_artist"},{"emoji":"🧑✈️","skin_tone_support":true,"name":"pilot","slug":"pilot"},{"emoji":"👨✈️","skin_tone_support":true,"name":"man pilot","slug":"man_pilot"},{"emoji":"👩✈️","skin_tone_support":true,"name":"woman pilot","slug":"woman_pilot"},{"emoji":"🧑🚀","skin_tone_support":true,"name":"astronaut","slug":"astronaut"},{"emoji":"👨🚀","skin_tone_support":true,"name":"man astronaut","slug":"man_astronaut"},{"emoji":"👩🚀","skin_tone_support":true,"name":"woman astronaut","slug":"woman_astronaut"},{"emoji":"🧑🚒","skin_tone_support":true,"name":"firefighter","slug":"firefighter"},{"emoji":"👨🚒","skin_tone_support":true,"name":"man firefighter","slug":"man_firefighter"},{"emoji":"👩🚒","skin_tone_support":true,"name":"woman firefighter","slug":"woman_firefighter"},{"emoji":"👮","skin_tone_support":true,"name":"police officer","slug":"police_officer"},{"emoji":"👮♂️","skin_tone_support":true,"name":"man police officer","slug":"man_police_officer"},{"emoji":"👮♀️","skin_tone_support":true,"name":"woman police officer","slug":"woman_police_officer"},{"emoji":"🕵️","skin_tone_support":true,"name":"detective","slug":"detective"},{"emoji":"🕵️♂️","skin_tone_support":true,"name":"man detective","slug":"man_detective"},{"emoji":"🕵️♀️","skin_tone_support":true,"name":"woman detective","slug":"woman_detective"},{"emoji":"💂","skin_tone_support":true,"name":"guard","slug":"guard"},{"emoji":"💂♂️","skin_tone_support":true,"name":"man guard","slug":"man_guard"},{"emoji":"💂♀️","skin_tone_support":true,"name":"woman guard","slug":"woman_guard"},{"emoji":"🥷","skin_tone_support":true,"name":"ninja","slug":"ninja"},{"emoji":"👷","skin_tone_support":true,"name":"construction worker","slug":"construction_worker"},{"emoji":"👷♂️","skin_tone_support":true,"name":"man construction worker","slug":"man_construction_worker"},{"emoji":"👷♀️","skin_tone_support":true,"name":"woman construction worker","slug":"woman_construction_worker"},{"emoji":"🫅","skin_tone_support":true,"name":"person with crown","slug":"person_with_crown"},{"emoji":"🤴","skin_tone_support":true,"name":"prince","slug":"prince"},{"emoji":"👸","skin_tone_support":true,"name":"princess","slug":"princess"},{"emoji":"👳","skin_tone_support":true,"name":"person wearing turban","slug":"person_wearing_turban"},{"emoji":"👳♂️","skin_tone_support":true,"name":"man wearing turban","slug":"man_wearing_turban"},{"emoji":"👳♀️","skin_tone_support":true,"name":"woman wearing turban","slug":"woman_wearing_turban"},{"emoji":"👲","skin_tone_support":true,"name":"person with skullcap","slug":"person_with_skullcap"},{"emoji":"🧕","skin_tone_support":true,"name":"woman with headscarf","slug":"woman_with_headscarf"},{"emoji":"🤵","skin_tone_support":true,"name":"person in tuxedo","slug":"person_in_tuxedo"},{"emoji":"🤵♂️","skin_tone_support":true,"name":"man in tuxedo","slug":"man_in_tuxedo"},{"emoji":"🤵♀️","skin_tone_support":true,"name":"woman in tuxedo","slug":"woman_in_tuxedo"},{"emoji":"👰","skin_tone_support":true,"name":"person with veil","slug":"person_with_veil"},{"emoji":"👰♂️","skin_tone_support":true,"name":"man with veil","slug":"man_with_veil"},{"emoji":"👰♀️","skin_tone_support":true,"name":"woman with veil","slug":"woman_with_veil"},{"emoji":"🤰","skin_tone_support":true,"name":"pregnant woman","slug":"pregnant_woman"},{"emoji":"🫃","skin_tone_support":true,"name":"pregnant man","slug":"pregnant_man"},{"emoji":"🫄","skin_tone_support":true,"name":"pregnant person","slug":"pregnant_person"},{"emoji":"🤱","skin_tone_support":true,"name":"breast-feeding","slug":"breast_feeding"},{"emoji":"👩🍼","skin_tone_support":true,"name":"woman feeding baby","slug":"woman_feeding_baby"},{"emoji":"👨🍼","skin_tone_support":true,"name":"man feeding baby","slug":"man_feeding_baby"},{"emoji":"🧑🍼","skin_tone_support":true,"name":"person feeding baby","slug":"person_feeding_baby"},{"emoji":"👼","skin_tone_support":true,"name":"baby angel","slug":"baby_angel"},{"emoji":"🎅","skin_tone_support":true,"name":"Santa Claus","slug":"santa_claus"},{"emoji":"🤶","skin_tone_support":true,"name":"Mrs. Claus","slug":"mrs_claus"},{"emoji":"🧑🎄","skin_tone_support":true,"name":"mx claus","slug":"mx_claus"},{"emoji":"🦸","skin_tone_support":true,"name":"superhero","slug":"superhero"},{"emoji":"🦸♂️","skin_tone_support":true,"name":"man superhero","slug":"man_superhero"},{"emoji":"🦸♀️","skin_tone_support":true,"name":"woman superhero","slug":"woman_superhero"},{"emoji":"🦹","skin_tone_support":true,"name":"supervillain","slug":"supervillain"},{"emoji":"🦹♂️","skin_tone_support":true,"name":"man supervillain","slug":"man_supervillain"},{"emoji":"🦹♀️","skin_tone_support":true,"name":"woman supervillain","slug":"woman_supervillain"},{"emoji":"🧙","skin_tone_support":true,"name":"mage","slug":"mage"},{"emoji":"🧙♂️","skin_tone_support":true,"name":"man mage","slug":"man_mage"},{"emoji":"🧙♀️","skin_tone_support":true,"name":"woman mage","slug":"woman_mage"},{"emoji":"🧚","skin_tone_support":true,"name":"fairy","slug":"fairy"},{"emoji":"🧚♂️","skin_tone_support":true,"name":"man fairy","slug":"man_fairy"},{"emoji":"🧚♀️","skin_tone_support":true,"name":"woman fairy","slug":"woman_fairy"},{"emoji":"🧛","skin_tone_support":true,"name":"vampire","slug":"vampire"},{"emoji":"🧛♂️","skin_tone_support":true,"name":"man vampire","slug":"man_vampire"},{"emoji":"🧛♀️","skin_tone_support":true,"name":"woman vampire","slug":"woman_vampire"},{"emoji":"🧜","skin_tone_support":true,"name":"merperson","slug":"merperson"},{"emoji":"🧜♂️","skin_tone_support":true,"name":"merman","slug":"merman"},{"emoji":"🧜♀️","skin_tone_support":true,"name":"mermaid","slug":"mermaid"},{"emoji":"🧝","skin_tone_support":true,"name":"elf","slug":"elf"},{"emoji":"🧝♂️","skin_tone_support":true,"name":"man elf","slug":"man_elf"},{"emoji":"🧝♀️","skin_tone_support":true,"name":"woman elf","slug":"woman_elf"},{"emoji":"🧞","skin_tone_support":false,"name":"genie","slug":"genie"},{"emoji":"🧞♂️","skin_tone_support":false,"name":"man genie","slug":"man_genie"},{"emoji":"🧞♀️","skin_tone_support":false,"name":"woman genie","slug":"woman_genie"},{"emoji":"🧟","skin_tone_support":false,"name":"zombie","slug":"zombie"},{"emoji":"🧟♂️","skin_tone_support":false,"name":"man zombie","slug":"man_zombie"},{"emoji":"🧟♀️","skin_tone_support":false,"name":"woman zombie","slug":"woman_zombie"},{"emoji":"🧌","skin_tone_support":false,"name":"troll","slug":"troll"},{"emoji":"💆","skin_tone_support":true,"name":"person getting massage","slug":"person_getting_massage"},{"emoji":"💆♂️","skin_tone_support":true,"name":"man getting massage","slug":"man_getting_massage"},{"emoji":"💆♀️","skin_tone_support":true,"name":"woman getting massage","slug":"woman_getting_massage"},{"emoji":"💇","skin_tone_support":true,"name":"person getting haircut","slug":"person_getting_haircut"},{"emoji":"💇♂️","skin_tone_support":true,"name":"man getting haircut","slug":"man_getting_haircut"},{"emoji":"💇♀️","skin_tone_support":true,"name":"woman getting haircut","slug":"woman_getting_haircut"},{"emoji":"🚶","skin_tone_support":true,"name":"person walking","slug":"person_walking"},{"emoji":"🚶♂️","skin_tone_support":true,"name":"man walking","slug":"man_walking"},{"emoji":"🚶♀️","skin_tone_support":true,"name":"woman walking","slug":"woman_walking"},{"emoji":"🧍","skin_tone_support":true,"name":"person standing","slug":"person_standing"},{"emoji":"🧍♂️","skin_tone_support":true,"name":"man standing","slug":"man_standing"},{"emoji":"🧍♀️","skin_tone_support":true,"name":"woman standing","slug":"woman_standing"},{"emoji":"🧎","skin_tone_support":true,"name":"person kneeling","slug":"person_kneeling"},{"emoji":"🧎♂️","skin_tone_support":true,"name":"man kneeling","slug":"man_kneeling"},{"emoji":"🧎♀️","skin_tone_support":true,"name":"woman kneeling","slug":"woman_kneeling"},{"emoji":"🧑🦯","skin_tone_support":true,"name":"person with white cane","slug":"person_with_white_cane"},{"emoji":"👨🦯","skin_tone_support":true,"name":"man with white cane","slug":"man_with_white_cane"},{"emoji":"👩🦯","skin_tone_support":true,"name":"woman with white cane","slug":"woman_with_white_cane"},{"emoji":"🧑🦼","skin_tone_support":true,"name":"person in motorized wheelchair","slug":"person_in_motorized_wheelchair"},{"emoji":"👨🦼","skin_tone_support":true,"name":"man in motorized wheelchair","slug":"man_in_motorized_wheelchair"},{"emoji":"👩🦼","skin_tone_support":true,"name":"woman in motorized wheelchair","slug":"woman_in_motorized_wheelchair"},{"emoji":"🧑🦽","skin_tone_support":true,"name":"person in manual wheelchair","slug":"person_in_manual_wheelchair"},{"emoji":"👨🦽","skin_tone_support":true,"name":"man in manual wheelchair","slug":"man_in_manual_wheelchair"},{"emoji":"👩🦽","skin_tone_support":true,"name":"woman in manual wheelchair","slug":"woman_in_manual_wheelchair"},{"emoji":"🏃","skin_tone_support":true,"name":"person running","slug":"person_running"},{"emoji":"🏃♂️","skin_tone_support":true,"name":"man running","slug":"man_running"},{"emoji":"🏃♀️","skin_tone_support":true,"name":"woman running","slug":"woman_running"},{"emoji":"💃","skin_tone_support":true,"name":"woman dancing","slug":"woman_dancing"},{"emoji":"🕺","skin_tone_support":true,"name":"man dancing","slug":"man_dancing"},{"emoji":"🕴️","skin_tone_support":true,"name":"person in suit levitating","slug":"person_in_suit_levitating"},{"emoji":"👯","skin_tone_support":false,"name":"people with bunny ears","slug":"people_with_bunny_ears"},{"emoji":"👯♂️","skin_tone_support":false,"name":"men with bunny ears","slug":"men_with_bunny_ears"},{"emoji":"👯♀️","skin_tone_support":false,"name":"women with bunny ears","slug":"women_with_bunny_ears"},{"emoji":"🧖","skin_tone_support":true,"name":"person in steamy room","slug":"person_in_steamy_room"},{"emoji":"🧖♂️","skin_tone_support":true,"name":"man in steamy room","slug":"man_in_steamy_room"},{"emoji":"🧖♀️","skin_tone_support":true,"name":"woman in steamy room","slug":"woman_in_steamy_room"},{"emoji":"🧗","skin_tone_support":true,"name":"person climbing","slug":"person_climbing"},{"emoji":"🧗♂️","skin_tone_support":true,"name":"man climbing","slug":"man_climbing"},{"emoji":"🧗♀️","skin_tone_support":true,"name":"woman climbing","slug":"woman_climbing"},{"emoji":"🤺","skin_tone_support":false,"name":"person fencing","slug":"person_fencing"},{"emoji":"🏇","skin_tone_support":true,"name":"horse racing","slug":"horse_racing"},{"emoji":"⛷️","skin_tone_support":false,"name":"skier","slug":"skier"},{"emoji":"🏂","skin_tone_support":true,"name":"snowboarder","slug":"snowboarder"},{"emoji":"🏌️","skin_tone_support":true,"name":"person golfing","slug":"person_golfing"},{"emoji":"🏌️♂️","skin_tone_support":true,"name":"man golfing","slug":"man_golfing"},{"emoji":"🏌️♀️","skin_tone_support":true,"name":"woman golfing","slug":"woman_golfing"},{"emoji":"🏄","skin_tone_support":true,"name":"person surfing","slug":"person_surfing"},{"emoji":"🏄♂️","skin_tone_support":true,"name":"man surfing","slug":"man_surfing"},{"emoji":"🏄♀️","skin_tone_support":true,"name":"woman surfing","slug":"woman_surfing"},{"emoji":"🚣","skin_tone_support":true,"name":"person rowing boat","slug":"person_rowing_boat"},{"emoji":"🚣♂️","skin_tone_support":true,"name":"man rowing boat","slug":"man_rowing_boat"},{"emoji":"🚣♀️","skin_tone_support":true,"name":"woman rowing boat","slug":"woman_rowing_boat"},{"emoji":"🏊","skin_tone_support":true,"name":"person swimming","slug":"person_swimming"},{"emoji":"🏊♂️","skin_tone_support":true,"name":"man swimming","slug":"man_swimming"},{"emoji":"🏊♀️","skin_tone_support":true,"name":"woman swimming","slug":"woman_swimming"},{"emoji":"⛹️","skin_tone_support":true,"name":"person bouncing ball","slug":"person_bouncing_ball"},{"emoji":"⛹️♂️","skin_tone_support":true,"name":"man bouncing ball","slug":"man_bouncing_ball"},{"emoji":"⛹️♀️","skin_tone_support":true,"name":"woman bouncing ball","slug":"woman_bouncing_ball"},{"emoji":"🏋️","skin_tone_support":true,"name":"person lifting weights","slug":"person_lifting_weights"},{"emoji":"🏋️♂️","skin_tone_support":true,"name":"man lifting weights","slug":"man_lifting_weights"},{"emoji":"🏋️♀️","skin_tone_support":true,"name":"woman lifting weights","slug":"woman_lifting_weights"},{"emoji":"🚴","skin_tone_support":true,"name":"person biking","slug":"person_biking"},{"emoji":"🚴♂️","skin_tone_support":true,"name":"man biking","slug":"man_biking"},{"emoji":"🚴♀️","skin_tone_support":true,"name":"woman biking","slug":"woman_biking"},{"emoji":"🚵","skin_tone_support":true,"name":"person mountain biking","slug":"person_mountain_biking"},{"emoji":"🚵♂️","skin_tone_support":true,"name":"man mountain biking","slug":"man_mountain_biking"},{"emoji":"🚵♀️","skin_tone_support":true,"name":"woman mountain biking","slug":"woman_mountain_biking"},{"emoji":"🤸","skin_tone_support":true,"name":"person cartwheeling","slug":"person_cartwheeling"},{"emoji":"🤸♂️","skin_tone_support":true,"name":"man cartwheeling","slug":"man_cartwheeling"},{"emoji":"🤸♀️","skin_tone_support":true,"name":"woman cartwheeling","slug":"woman_cartwheeling"},{"emoji":"🤼","skin_tone_support":false,"name":"people wrestling","slug":"people_wrestling"},{"emoji":"🤼♂️","skin_tone_support":false,"name":"men wrestling","slug":"men_wrestling"},{"emoji":"🤼♀️","skin_tone_support":false,"name":"women wrestling","slug":"women_wrestling"},{"emoji":"🤽","skin_tone_support":true,"name":"person playing water polo","slug":"person_playing_water_polo"},{"emoji":"🤽♂️","skin_tone_support":true,"name":"man playing water polo","slug":"man_playing_water_polo"},{"emoji":"🤽♀️","skin_tone_support":true,"name":"woman playing water polo","slug":"woman_playing_water_polo"},{"emoji":"🤾","skin_tone_support":true,"name":"person playing handball","slug":"person_playing_handball"},{"emoji":"🤾♂️","skin_tone_support":true,"name":"man playing handball","slug":"man_playing_handball"},{"emoji":"🤾♀️","skin_tone_support":true,"name":"woman playing handball","slug":"woman_playing_handball"},{"emoji":"🤹","skin_tone_support":true,"name":"person juggling","slug":"person_juggling"},{"emoji":"🤹♂️","skin_tone_support":true,"name":"man juggling","slug":"man_juggling"},{"emoji":"🤹♀️","skin_tone_support":true,"name":"woman juggling","slug":"woman_juggling"},{"emoji":"🧘","skin_tone_support":true,"name":"person in lotus position","slug":"person_in_lotus_position"},{"emoji":"🧘♂️","skin_tone_support":true,"name":"man in lotus position","slug":"man_in_lotus_position"},{"emoji":"🧘♀️","skin_tone_support":true,"name":"woman in lotus position","slug":"woman_in_lotus_position"},{"emoji":"🛀","skin_tone_support":true,"name":"person taking bath","slug":"person_taking_bath"},{"emoji":"🛌","skin_tone_support":true,"name":"person in bed","slug":"person_in_bed"},{"emoji":"🧑🤝🧑","skin_tone_support":true,"name":"people holding hands","slug":"people_holding_hands"},{"emoji":"👭","skin_tone_support":true,"name":"women holding hands","slug":"women_holding_hands"},{"emoji":"👫","skin_tone_support":true,"name":"woman and man holding hands","slug":"woman_and_man_holding_hands"},{"emoji":"👬","skin_tone_support":true,"name":"men holding hands","slug":"men_holding_hands"},{"emoji":"💏","skin_tone_support":true,"name":"kiss","slug":"kiss"},{"emoji":"👩❤️💋👨","skin_tone_support":true,"name":"kiss woman, man","slug":"kiss_woman_man"},{"emoji":"👨❤️💋👨","skin_tone_support":true,"name":"kiss man, man","slug":"kiss_man_man"},{"emoji":"👩❤️💋👩","skin_tone_support":true,"name":"kiss woman, woman","slug":"kiss_woman_woman"},{"emoji":"💑","skin_tone_support":true,"name":"couple with heart","slug":"couple_with_heart"},{"emoji":"👩❤️👨","skin_tone_support":true,"name":"couple with heart woman, man","slug":"couple_with_heart_woman_man"},{"emoji":"👨❤️👨","skin_tone_support":true,"name":"couple with heart man, man","slug":"couple_with_heart_man_man"},{"emoji":"👩❤️👩","skin_tone_support":true,"name":"couple with heart woman, woman","slug":"couple_with_heart_woman_woman"},{"emoji":"👪","skin_tone_support":false,"name":"family","slug":"family"},{"emoji":"👨👩👦","skin_tone_support":false,"name":"family man, woman, boy","slug":"family_man_woman_boy"},{"emoji":"👨👩👧","skin_tone_support":false,"name":"family man, woman, girl","slug":"family_man_woman_girl"},{"emoji":"👨👩👧👦","skin_tone_support":false,"name":"family man, woman, girl, boy","slug":"family_man_woman_girl_boy"},{"emoji":"👨👩👦👦","skin_tone_support":false,"name":"family man, woman, boy, boy","slug":"family_man_woman_boy_boy"},{"emoji":"👨👩👧👧","skin_tone_support":false,"name":"family man, woman, girl, girl","slug":"family_man_woman_girl_girl"},{"emoji":"👨👨👦","skin_tone_support":false,"name":"family man, man, boy","slug":"family_man_man_boy"},{"emoji":"👨👨👧","skin_tone_support":false,"name":"family man, man, girl","slug":"family_man_man_girl"},{"emoji":"👨👨👧👦","skin_tone_support":false,"name":"family man, man, girl, boy","slug":"family_man_man_girl_boy"},{"emoji":"👨👨👦👦","skin_tone_support":false,"name":"family man, man, boy, boy","slug":"family_man_man_boy_boy"},{"emoji":"👨👨👧👧","skin_tone_support":false,"name":"family man, man, girl, girl","slug":"family_man_man_girl_girl"},{"emoji":"👩👩👦","skin_tone_support":false,"name":"family woman, woman, boy","slug":"family_woman_woman_boy"},{"emoji":"👩👩👧","skin_tone_support":false,"name":"family woman, woman, girl","slug":"family_woman_woman_girl"},{"emoji":"👩👩👧👦","skin_tone_support":false,"name":"family woman, woman, girl, boy","slug":"family_woman_woman_girl_boy"},{"emoji":"👩👩👦👦","skin_tone_support":false,"name":"family woman, woman, boy, boy","slug":"family_woman_woman_boy_boy"},{"emoji":"👩👩👧👧","skin_tone_support":false,"name":"family woman, woman, girl, girl","slug":"family_woman_woman_girl_girl"},{"emoji":"👨👦","skin_tone_support":false,"name":"family man, boy","slug":"family_man_boy"},{"emoji":"👨👦👦","skin_tone_support":false,"name":"family man, boy, boy","slug":"family_man_boy_boy"},{"emoji":"👨👧","skin_tone_support":false,"name":"family man, girl","slug":"family_man_girl"},{"emoji":"👨👧👦","skin_tone_support":false,"name":"family man, girl, boy","slug":"family_man_girl_boy"},{"emoji":"👨👧👧","skin_tone_support":false,"name":"family man, girl, girl","slug":"family_man_girl_girl"},{"emoji":"👩👦","skin_tone_support":false,"name":"family woman, boy","slug":"family_woman_boy"},{"emoji":"👩👦👦","skin_tone_support":false,"name":"family woman, boy, boy","slug":"family_woman_boy_boy"},{"emoji":"👩👧","skin_tone_support":false,"name":"family woman, girl","slug":"family_woman_girl"},{"emoji":"👩👧👦","skin_tone_support":false,"name":"family woman, girl, boy","slug":"family_woman_girl_boy"},{"emoji":"👩👧👧","skin_tone_support":false,"name":"family woman, girl, girl","slug":"family_woman_girl_girl"},{"emoji":"🗣️","skin_tone_support":false,"name":"speaking head","slug":"speaking_head"},{"emoji":"👤","skin_tone_support":false,"name":"bust in silhouette","slug":"bust_in_silhouette"},{"emoji":"👥","skin_tone_support":false,"name":"busts in silhouette","slug":"busts_in_silhouette"},{"emoji":"🫂","skin_tone_support":false,"name":"people hugging","slug":"people_hugging"},{"emoji":"👣","skin_tone_support":false,"name":"footprints","slug":"footprints"}],"animals-and-nature":[{"emoji":"🐵","skin_tone_support":false,"name":"monkey face","slug":"monkey_face"},{"emoji":"🐒","skin_tone_support":false,"name":"monkey","slug":"monkey"},{"emoji":"🦍","skin_tone_support":false,"name":"gorilla","slug":"gorilla"},{"emoji":"🦧","skin_tone_support":false,"name":"orangutan","slug":"orangutan"},{"emoji":"🐶","skin_tone_support":false,"name":"dog face","slug":"dog_face"},{"emoji":"🐕","skin_tone_support":false,"name":"dog","slug":"dog"},{"emoji":"🦮","skin_tone_support":false,"name":"guide dog","slug":"guide_dog"},{"emoji":"🐕🦺","skin_tone_support":false,"name":"service dog","slug":"service_dog"},{"emoji":"🐩","skin_tone_support":false,"name":"poodle","slug":"poodle"},{"emoji":"🐺","skin_tone_support":false,"name":"wolf","slug":"wolf"},{"emoji":"🦊","skin_tone_support":false,"name":"fox","slug":"fox"},{"emoji":"🦝","skin_tone_support":false,"name":"raccoon","slug":"raccoon"},{"emoji":"🐱","skin_tone_support":false,"name":"cat face","slug":"cat_face"},{"emoji":"🐈","skin_tone_support":false,"name":"cat","slug":"cat"},{"emoji":"🐈⬛","skin_tone_support":false,"name":"black cat","slug":"black_cat"},{"emoji":"🦁","skin_tone_support":false,"name":"lion","slug":"lion"},{"emoji":"🐯","skin_tone_support":false,"name":"tiger face","slug":"tiger_face"},{"emoji":"🐅","skin_tone_support":false,"name":"tiger","slug":"tiger"},{"emoji":"🐆","skin_tone_support":false,"name":"leopard","slug":"leopard"},{"emoji":"🐴","skin_tone_support":false,"name":"horse face","slug":"horse_face"},{"emoji":"🐎","skin_tone_support":false,"name":"horse","slug":"horse"},{"emoji":"🦄","skin_tone_support":false,"name":"unicorn","slug":"unicorn"},{"emoji":"🦓","skin_tone_support":false,"name":"zebra","slug":"zebra"},{"emoji":"🦌","skin_tone_support":false,"name":"deer","slug":"deer"},{"emoji":"🦬","skin_tone_support":false,"name":"bison","slug":"bison"},{"emoji":"🐮","skin_tone_support":false,"name":"cow face","slug":"cow_face"},{"emoji":"🐂","skin_tone_support":false,"name":"ox","slug":"ox"},{"emoji":"🐃","skin_tone_support":false,"name":"water buffalo","slug":"water_buffalo"},{"emoji":"🐄","skin_tone_support":false,"name":"cow","slug":"cow"},{"emoji":"🐷","skin_tone_support":false,"name":"pig face","slug":"pig_face"},{"emoji":"🐖","skin_tone_support":false,"name":"pig","slug":"pig"},{"emoji":"🐗","skin_tone_support":false,"name":"boar","slug":"boar"},{"emoji":"🐽","skin_tone_support":false,"name":"pig nose","slug":"pig_nose"},{"emoji":"🐏","skin_tone_support":false,"name":"ram","slug":"ram"},{"emoji":"🐑","skin_tone_support":false,"name":"ewe","slug":"ewe"},{"emoji":"🐐","skin_tone_support":false,"name":"goat","slug":"goat"},{"emoji":"🐪","skin_tone_support":false,"name":"camel","slug":"camel"},{"emoji":"🐫","skin_tone_support":false,"name":"two-hump camel","slug":"two_hump_camel"},{"emoji":"🦙","skin_tone_support":false,"name":"llama","slug":"llama"},{"emoji":"🦒","skin_tone_support":false,"name":"giraffe","slug":"giraffe"},{"emoji":"🐘","skin_tone_support":false,"name":"elephant","slug":"elephant"},{"emoji":"🦣","skin_tone_support":false,"name":"mammoth","slug":"mammoth"},{"emoji":"🦏","skin_tone_support":false,"name":"rhinoceros","slug":"rhinoceros"},{"emoji":"🦛","skin_tone_support":false,"name":"hippopotamus","slug":"hippopotamus"},{"emoji":"🐭","skin_tone_support":false,"name":"mouse face","slug":"mouse_face"},{"emoji":"🐁","skin_tone_support":false,"name":"mouse","slug":"mouse"},{"emoji":"🐀","skin_tone_support":false,"name":"rat","slug":"rat"},{"emoji":"🐹","skin_tone_support":false,"name":"hamster","slug":"hamster"},{"emoji":"🐰","skin_tone_support":false,"name":"rabbit face","slug":"rabbit_face"},{"emoji":"🐇","skin_tone_support":false,"name":"rabbit","slug":"rabbit"},{"emoji":"🐿️","skin_tone_support":false,"name":"chipmunk","slug":"chipmunk"},{"emoji":"🦫","skin_tone_support":false,"name":"beaver","slug":"beaver"},{"emoji":"🦔","skin_tone_support":false,"name":"hedgehog","slug":"hedgehog"},{"emoji":"🦇","skin_tone_support":false,"name":"bat","slug":"bat"},{"emoji":"🐻","skin_tone_support":false,"name":"bear","slug":"bear"},{"emoji":"🐻❄️","skin_tone_support":false,"name":"polar bear","slug":"polar_bear"},{"emoji":"🐨","skin_tone_support":false,"name":"koala","slug":"koala"},{"emoji":"🐼","skin_tone_support":false,"name":"panda","slug":"panda"},{"emoji":"🦥","skin_tone_support":false,"name":"sloth","slug":"sloth"},{"emoji":"🦦","skin_tone_support":false,"name":"otter","slug":"otter"},{"emoji":"🦨","skin_tone_support":false,"name":"skunk","slug":"skunk"},{"emoji":"🦘","skin_tone_support":false,"name":"kangaroo","slug":"kangaroo"},{"emoji":"🦡","skin_tone_support":false,"name":"badger","slug":"badger"},{"emoji":"🐾","skin_tone_support":false,"name":"paw prints","slug":"paw_prints"},{"emoji":"🦃","skin_tone_support":false,"name":"turkey","slug":"turkey"},{"emoji":"🐔","skin_tone_support":false,"name":"chicken","slug":"chicken"},{"emoji":"🐓","skin_tone_support":false,"name":"rooster","slug":"rooster"},{"emoji":"🐣","skin_tone_support":false,"name":"hatching chick","slug":"hatching_chick"},{"emoji":"🐤","skin_tone_support":false,"name":"baby chick","slug":"baby_chick"},{"emoji":"🐥","skin_tone_support":false,"name":"front-facing baby chick","slug":"front_facing_baby_chick"},{"emoji":"🐦","skin_tone_support":false,"name":"bird","slug":"bird"},{"emoji":"🐧","skin_tone_support":false,"name":"penguin","slug":"penguin"},{"emoji":"🕊️","skin_tone_support":false,"name":"dove","slug":"dove"},{"emoji":"🦅","skin_tone_support":false,"name":"eagle","slug":"eagle"},{"emoji":"🦆","skin_tone_support":false,"name":"duck","slug":"duck"},{"emoji":"🦢","skin_tone_support":false,"name":"swan","slug":"swan"},{"emoji":"🦉","skin_tone_support":false,"name":"owl","slug":"owl"},{"emoji":"🦤","skin_tone_support":false,"name":"dodo","slug":"dodo"},{"emoji":"🪶","skin_tone_support":false,"name":"feather","slug":"feather"},{"emoji":"🦩","skin_tone_support":false,"name":"flamingo","slug":"flamingo"},{"emoji":"🦚","skin_tone_support":false,"name":"peacock","slug":"peacock"},{"emoji":"🦜","skin_tone_support":false,"name":"parrot","slug":"parrot"},{"emoji":"🐸","skin_tone_support":false,"name":"frog","slug":"frog"},{"emoji":"🐊","skin_tone_support":false,"name":"crocodile","slug":"crocodile"},{"emoji":"🐢","skin_tone_support":false,"name":"turtle","slug":"turtle"},{"emoji":"🦎","skin_tone_support":false,"name":"lizard","slug":"lizard"},{"emoji":"🐍","skin_tone_support":false,"name":"snake","slug":"snake"},{"emoji":"🐲","skin_tone_support":false,"name":"dragon face","slug":"dragon_face"},{"emoji":"🐉","skin_tone_support":false,"name":"dragon","slug":"dragon"},{"emoji":"🦕","skin_tone_support":false,"name":"sauropod","slug":"sauropod"},{"emoji":"🦖","skin_tone_support":false,"name":"T-Rex","slug":"t_rex"},{"emoji":"🐳","skin_tone_support":false,"name":"spouting whale","slug":"spouting_whale"},{"emoji":"🐋","skin_tone_support":false,"name":"whale","slug":"whale"},{"emoji":"🐬","skin_tone_support":false,"name":"dolphin","slug":"dolphin"},{"emoji":"🦭","skin_tone_support":false,"name":"seal","slug":"seal"},{"emoji":"🐟","skin_tone_support":false,"name":"fish","slug":"fish"},{"emoji":"🐠","skin_tone_support":false,"name":"tropical fish","slug":"tropical_fish"},{"emoji":"🐡","skin_tone_support":false,"name":"blowfish","slug":"blowfish"},{"emoji":"🦈","skin_tone_support":false,"name":"shark","slug":"shark"},{"emoji":"🐙","skin_tone_support":false,"name":"octopus","slug":"octopus"},{"emoji":"🐚","skin_tone_support":false,"name":"spiral shell","slug":"spiral_shell"},{"emoji":"🪸","skin_tone_support":false,"name":"coral","slug":"coral"},{"emoji":"🐌","skin_tone_support":false,"name":"snail","slug":"snail"},{"emoji":"🦋","skin_tone_support":false,"name":"butterfly","slug":"butterfly"},{"emoji":"🐛","skin_tone_support":false,"name":"bug","slug":"bug"},{"emoji":"🐜","skin_tone_support":false,"name":"ant","slug":"ant"},{"emoji":"🐝","skin_tone_support":false,"name":"honeybee","slug":"honeybee"},{"emoji":"🪲","skin_tone_support":false,"name":"beetle","slug":"beetle"},{"emoji":"🐞","skin_tone_support":false,"name":"lady beetle","slug":"lady_beetle"},{"emoji":"🦗","skin_tone_support":false,"name":"cricket","slug":"cricket"},{"emoji":"🪳","skin_tone_support":false,"name":"cockroach","slug":"cockroach"},{"emoji":"🕷️","skin_tone_support":false,"name":"spider","slug":"spider"},{"emoji":"🕸️","skin_tone_support":false,"name":"spider web","slug":"spider_web"},{"emoji":"🦂","skin_tone_support":false,"name":"scorpion","slug":"scorpion"},{"emoji":"🦟","skin_tone_support":false,"name":"mosquito","slug":"mosquito"},{"emoji":"🪰","skin_tone_support":false,"name":"fly","slug":"fly"},{"emoji":"🪱","skin_tone_support":false,"name":"worm","slug":"worm"},{"emoji":"🦠","skin_tone_support":false,"name":"microbe","slug":"microbe"},{"emoji":"💐","skin_tone_support":false,"name":"bouquet","slug":"bouquet"},{"emoji":"🌸","skin_tone_support":false,"name":"cherry blossom","slug":"cherry_blossom"},{"emoji":"💮","skin_tone_support":false,"name":"white flower","slug":"white_flower"},{"emoji":"🪷","skin_tone_support":false,"name":"lotus","slug":"lotus"},{"emoji":"🏵️","skin_tone_support":false,"name":"rosette","slug":"rosette"},{"emoji":"🌹","skin_tone_support":false,"name":"rose","slug":"rose"},{"emoji":"🥀","skin_tone_support":false,"name":"wilted flower","slug":"wilted_flower"},{"emoji":"🌺","skin_tone_support":false,"name":"hibiscus","slug":"hibiscus"},{"emoji":"🌻","skin_tone_support":false,"name":"sunflower","slug":"sunflower"},{"emoji":"🌼","skin_tone_support":false,"name":"blossom","slug":"blossom"},{"emoji":"🌷","skin_tone_support":false,"name":"tulip","slug":"tulip"},{"emoji":"🌱","skin_tone_support":false,"name":"seedling","slug":"seedling"},{"emoji":"🪴","skin_tone_support":false,"name":"potted plant","slug":"potted_plant"},{"emoji":"🌲","skin_tone_support":false,"name":"evergreen tree","slug":"evergreen_tree"},{"emoji":"🌳","skin_tone_support":false,"name":"deciduous tree","slug":"deciduous_tree"},{"emoji":"🌴","skin_tone_support":false,"name":"palm tree","slug":"palm_tree"},{"emoji":"🌵","skin_tone_support":false,"name":"cactus","slug":"cactus"},{"emoji":"🌾","skin_tone_support":false,"name":"sheaf of rice","slug":"sheaf_of_rice"},{"emoji":"🌿","skin_tone_support":false,"name":"herb","slug":"herb"},{"emoji":"☘️","skin_tone_support":false,"name":"shamrock","slug":"shamrock"},{"emoji":"🍀","skin_tone_support":false,"name":"four leaf clover","slug":"four_leaf_clover"},{"emoji":"🍁","skin_tone_support":false,"name":"maple leaf","slug":"maple_leaf"},{"emoji":"🍂","skin_tone_support":false,"name":"fallen leaf","slug":"fallen_leaf"},{"emoji":"🍃","skin_tone_support":false,"name":"leaf fluttering in wind","slug":"leaf_fluttering_in_wind"},{"emoji":"🪹","skin_tone_support":false,"name":"empty nest","slug":"empty_nest"},{"emoji":"🪺","skin_tone_support":false,"name":"nest with eggs","slug":"nest_with_eggs"}],"food-and-drink":[{"emoji":"🍇","skin_tone_support":false,"name":"grapes","slug":"grapes"},{"emoji":"🍈","skin_tone_support":false,"name":"melon","slug":"melon"},{"emoji":"🍉","skin_tone_support":false,"name":"watermelon","slug":"watermelon"},{"emoji":"🍊","skin_tone_support":false,"name":"tangerine","slug":"tangerine"},{"emoji":"🍋","skin_tone_support":false,"name":"lemon","slug":"lemon"},{"emoji":"🍌","skin_tone_support":false,"name":"banana","slug":"banana"},{"emoji":"🍍","skin_tone_support":false,"name":"pineapple","slug":"pineapple"},{"emoji":"🥭","skin_tone_support":false,"name":"mango","slug":"mango"},{"emoji":"🍎","skin_tone_support":false,"name":"red apple","slug":"red_apple"},{"emoji":"🍏","skin_tone_support":false,"name":"green apple","slug":"green_apple"},{"emoji":"🍐","skin_tone_support":false,"name":"pear","slug":"pear"},{"emoji":"🍑","skin_tone_support":false,"name":"peach","slug":"peach"},{"emoji":"🍒","skin_tone_support":false,"name":"cherries","slug":"cherries"},{"emoji":"🍓","skin_tone_support":false,"name":"strawberry","slug":"strawberry"},{"emoji":"🫐","skin_tone_support":false,"name":"blueberries","slug":"blueberries"},{"emoji":"🥝","skin_tone_support":false,"name":"kiwi fruit","slug":"kiwi_fruit"},{"emoji":"🍅","skin_tone_support":false,"name":"tomato","slug":"tomato"},{"emoji":"🫒","skin_tone_support":false,"name":"olive","slug":"olive"},{"emoji":"🥥","skin_tone_support":false,"name":"coconut","slug":"coconut"},{"emoji":"🥑","skin_tone_support":false,"name":"avocado","slug":"avocado"},{"emoji":"🍆","skin_tone_support":false,"name":"eggplant","slug":"eggplant"},{"emoji":"🥔","skin_tone_support":false,"name":"potato","slug":"potato"},{"emoji":"🥕","skin_tone_support":false,"name":"carrot","slug":"carrot"},{"emoji":"🌽","skin_tone_support":false,"name":"ear of corn","slug":"ear_of_corn"},{"emoji":"🌶️","skin_tone_support":false,"name":"hot pepper","slug":"hot_pepper"},{"emoji":"🫑","skin_tone_support":false,"name":"bell pepper","slug":"bell_pepper"},{"emoji":"🥒","skin_tone_support":false,"name":"cucumber","slug":"cucumber"},{"emoji":"🥬","skin_tone_support":false,"name":"leafy green","slug":"leafy_green"},{"emoji":"🥦","skin_tone_support":false,"name":"broccoli","slug":"broccoli"},{"emoji":"🧄","skin_tone_support":false,"name":"garlic","slug":"garlic"},{"emoji":"🧅","skin_tone_support":false,"name":"onion","slug":"onion"},{"emoji":"🍄","skin_tone_support":false,"name":"mushroom","slug":"mushroom"},{"emoji":"🥜","skin_tone_support":false,"name":"peanuts","slug":"peanuts"},{"emoji":"🫘","skin_tone_support":false,"name":"beans","slug":"beans"},{"emoji":"🌰","skin_tone_support":false,"name":"chestnut","slug":"chestnut"},{"emoji":"🍞","skin_tone_support":false,"name":"bread","slug":"bread"},{"emoji":"🥐","skin_tone_support":false,"name":"croissant","slug":"croissant"},{"emoji":"🥖","skin_tone_support":false,"name":"baguette bread","slug":"baguette_bread"},{"emoji":"🫓","skin_tone_support":false,"name":"flatbread","slug":"flatbread"},{"emoji":"🥨","skin_tone_support":false,"name":"pretzel","slug":"pretzel"},{"emoji":"🥯","skin_tone_support":false,"name":"bagel","slug":"bagel"},{"emoji":"🥞","skin_tone_support":false,"name":"pancakes","slug":"pancakes"},{"emoji":"🧇","skin_tone_support":false,"name":"waffle","slug":"waffle"},{"emoji":"🧀","skin_tone_support":false,"name":"cheese wedge","slug":"cheese_wedge"},{"emoji":"🍖","skin_tone_support":false,"name":"meat on bone","slug":"meat_on_bone"},{"emoji":"🍗","skin_tone_support":false,"name":"poultry leg","slug":"poultry_leg"},{"emoji":"🥩","skin_tone_support":false,"name":"cut of meat","slug":"cut_of_meat"},{"emoji":"🥓","skin_tone_support":false,"name":"bacon","slug":"bacon"},{"emoji":"🍔","skin_tone_support":false,"name":"hamburger","slug":"hamburger"},{"emoji":"🍟","skin_tone_support":false,"name":"french fries","slug":"french_fries"},{"emoji":"🍕","skin_tone_support":false,"name":"pizza","slug":"pizza"},{"emoji":"🌭","skin_tone_support":false,"name":"hot dog","slug":"hot_dog"},{"emoji":"🥪","skin_tone_support":false,"name":"sandwich","slug":"sandwich"},{"emoji":"🌮","skin_tone_support":false,"name":"taco","slug":"taco"},{"emoji":"🌯","skin_tone_support":false,"name":"burrito","slug":"burrito"},{"emoji":"🫔","skin_tone_support":false,"name":"tamale","slug":"tamale"},{"emoji":"🥙","skin_tone_support":false,"name":"stuffed flatbread","slug":"stuffed_flatbread"},{"emoji":"🧆","skin_tone_support":false,"name":"falafel","slug":"falafel"},{"emoji":"🥚","skin_tone_support":false,"name":"egg","slug":"egg"},{"emoji":"🍳","skin_tone_support":false,"name":"cooking","slug":"cooking"},{"emoji":"🥘","skin_tone_support":false,"name":"shallow pan of food","slug":"shallow_pan_of_food"},{"emoji":"🍲","skin_tone_support":false,"name":"pot of food","slug":"pot_of_food"},{"emoji":"🫕","skin_tone_support":false,"name":"fondue","slug":"fondue"},{"emoji":"🥣","skin_tone_support":false,"name":"bowl with spoon","slug":"bowl_with_spoon"},{"emoji":"🥗","skin_tone_support":false,"name":"green salad","slug":"green_salad"},{"emoji":"🍿","skin_tone_support":false,"name":"popcorn","slug":"popcorn"},{"emoji":"🧈","skin_tone_support":false,"name":"butter","slug":"butter"},{"emoji":"🧂","skin_tone_support":false,"name":"salt","slug":"salt"},{"emoji":"🥫","skin_tone_support":false,"name":"canned food","slug":"canned_food"},{"emoji":"🍱","skin_tone_support":false,"name":"bento box","slug":"bento_box"},{"emoji":"🍘","skin_tone_support":false,"name":"rice cracker","slug":"rice_cracker"},{"emoji":"🍙","skin_tone_support":false,"name":"rice ball","slug":"rice_ball"},{"emoji":"🍚","skin_tone_support":false,"name":"cooked rice","slug":"cooked_rice"},{"emoji":"🍛","skin_tone_support":false,"name":"curry rice","slug":"curry_rice"},{"emoji":"🍜","skin_tone_support":false,"name":"steaming bowl","slug":"steaming_bowl"},{"emoji":"🍝","skin_tone_support":false,"name":"spaghetti","slug":"spaghetti"},{"emoji":"🍠","skin_tone_support":false,"name":"roasted sweet potato","slug":"roasted_sweet_potato"},{"emoji":"🍢","skin_tone_support":false,"name":"oden","slug":"oden"},{"emoji":"🍣","skin_tone_support":false,"name":"sushi","slug":"sushi"},{"emoji":"🍤","skin_tone_support":false,"name":"fried shrimp","slug":"fried_shrimp"},{"emoji":"🍥","skin_tone_support":false,"name":"fish cake with swirl","slug":"fish_cake_with_swirl"},{"emoji":"🥮","skin_tone_support":false,"name":"moon cake","slug":"moon_cake"},{"emoji":"🍡","skin_tone_support":false,"name":"dango","slug":"dango"},{"emoji":"🥟","skin_tone_support":false,"name":"dumpling","slug":"dumpling"},{"emoji":"🥠","skin_tone_support":false,"name":"fortune cookie","slug":"fortune_cookie"},{"emoji":"🥡","skin_tone_support":false,"name":"takeout box","slug":"takeout_box"},{"emoji":"🦀","skin_tone_support":false,"name":"crab","slug":"crab"},{"emoji":"🦞","skin_tone_support":false,"name":"lobster","slug":"lobster"},{"emoji":"🦐","skin_tone_support":false,"name":"shrimp","slug":"shrimp"},{"emoji":"🦑","skin_tone_support":false,"name":"squid","slug":"squid"},{"emoji":"🦪","skin_tone_support":false,"name":"oyster","slug":"oyster"},{"emoji":"🍦","skin_tone_support":false,"name":"soft ice cream","slug":"soft_ice_cream"},{"emoji":"🍧","skin_tone_support":false,"name":"shaved ice","slug":"shaved_ice"},{"emoji":"🍨","skin_tone_support":false,"name":"ice cream","slug":"ice_cream"},{"emoji":"🍩","skin_tone_support":false,"name":"doughnut","slug":"doughnut"},{"emoji":"🍪","skin_tone_support":false,"name":"cookie","slug":"cookie"},{"emoji":"🎂","skin_tone_support":false,"name":"birthday cake","slug":"birthday_cake"},{"emoji":"🍰","skin_tone_support":false,"name":"shortcake","slug":"shortcake"},{"emoji":"🧁","skin_tone_support":false,"name":"cupcake","slug":"cupcake"},{"emoji":"🥧","skin_tone_support":false,"name":"pie","slug":"pie"},{"emoji":"🍫","skin_tone_support":false,"name":"chocolate bar","slug":"chocolate_bar"},{"emoji":"🍬","skin_tone_support":false,"name":"candy","slug":"candy"},{"emoji":"🍭","skin_tone_support":false,"name":"lollipop","slug":"lollipop"},{"emoji":"🍮","skin_tone_support":false,"name":"custard","slug":"custard"},{"emoji":"🍯","skin_tone_support":false,"name":"honey pot","slug":"honey_pot"},{"emoji":"🍼","skin_tone_support":false,"name":"baby bottle","slug":"baby_bottle"},{"emoji":"🥛","skin_tone_support":false,"name":"glass of milk","slug":"glass_of_milk"},{"emoji":"☕","skin_tone_support":false,"name":"hot beverage","slug":"hot_beverage"},{"emoji":"🫖","skin_tone_support":false,"name":"teapot","slug":"teapot"},{"emoji":"🍵","skin_tone_support":false,"name":"teacup without handle","slug":"teacup_without_handle"},{"emoji":"🍶","skin_tone_support":false,"name":"sake","slug":"sake"},{"emoji":"🍾","skin_tone_support":false,"name":"bottle with popping cork","slug":"bottle_with_popping_cork"},{"emoji":"🍷","skin_tone_support":false,"name":"wine glass","slug":"wine_glass"},{"emoji":"🍸","skin_tone_support":false,"name":"cocktail glass","slug":"cocktail_glass"},{"emoji":"🍹","skin_tone_support":false,"name":"tropical drink","slug":"tropical_drink"},{"emoji":"🍺","skin_tone_support":false,"name":"beer mug","slug":"beer_mug"},{"emoji":"🍻","skin_tone_support":false,"name":"clinking beer mugs","slug":"clinking_beer_mugs"},{"emoji":"🥂","skin_tone_support":false,"name":"clinking glasses","slug":"clinking_glasses"},{"emoji":"🥃","skin_tone_support":false,"name":"tumbler glass","slug":"tumbler_glass"},{"emoji":"🫗","skin_tone_support":false,"name":"pouring liquid","slug":"pouring_liquid"},{"emoji":"🥤","skin_tone_support":false,"name":"cup with straw","slug":"cup_with_straw"},{"emoji":"🧋","skin_tone_support":false,"name":"bubble tea","slug":"bubble_tea"},{"emoji":"🧃","skin_tone_support":false,"name":"beverage box","slug":"beverage_box"},{"emoji":"🧉","skin_tone_support":false,"name":"mate","slug":"mate"},{"emoji":"🧊","skin_tone_support":false,"name":"ice","slug":"ice"},{"emoji":"🥢","skin_tone_support":false,"name":"chopsticks","slug":"chopsticks"},{"emoji":"🍽️","skin_tone_support":false,"name":"fork and knife with plate","slug":"fork_and_knife_with_plate"},{"emoji":"🍴","skin_tone_support":false,"name":"fork and knife","slug":"fork_and_knife"},{"emoji":"🥄","skin_tone_support":false,"name":"spoon","slug":"spoon"},{"emoji":"🔪","skin_tone_support":false,"name":"kitchen knife","slug":"kitchen_knife"},{"emoji":"🫙","skin_tone_support":false,"name":"jar","slug":"jar"},{"emoji":"🏺","skin_tone_support":false,"name":"amphora","slug":"amphora"}],"travel-and-places":[{"emoji":"🌍","skin_tone_support":false,"name":"globe showing Europe-Africa","slug":"globe_showing_europe_africa"},{"emoji":"🌎","skin_tone_support":false,"name":"globe showing Americas","slug":"globe_showing_americas"},{"emoji":"🌏","skin_tone_support":false,"name":"globe showing Asia-Australia","slug":"globe_showing_asia_australia"},{"emoji":"🌐","skin_tone_support":false,"name":"globe with meridians","slug":"globe_with_meridians"},{"emoji":"🗺️","skin_tone_support":false,"name":"world map","slug":"world_map"},{"emoji":"🗾","skin_tone_support":false,"name":"map of Japan","slug":"map_of_japan"},{"emoji":"🧭","skin_tone_support":false,"name":"compass","slug":"compass"},{"emoji":"🏔️","skin_tone_support":false,"name":"snow-capped mountain","slug":"snow_capped_mountain"},{"emoji":"⛰️","skin_tone_support":false,"name":"mountain","slug":"mountain"},{"emoji":"🌋","skin_tone_support":false,"name":"volcano","slug":"volcano"},{"emoji":"🗻","skin_tone_support":false,"name":"mount fuji","slug":"mount_fuji"},{"emoji":"🏕️","skin_tone_support":false,"name":"camping","slug":"camping"},{"emoji":"🏖️","skin_tone_support":false,"name":"beach with umbrella","slug":"beach_with_umbrella"},{"emoji":"🏜️","skin_tone_support":false,"name":"desert","slug":"desert"},{"emoji":"🏝️","skin_tone_support":false,"name":"desert island","slug":"desert_island"},{"emoji":"🏞️","skin_tone_support":false,"name":"national park","slug":"national_park"},{"emoji":"🏟️","skin_tone_support":false,"name":"stadium","slug":"stadium"},{"emoji":"🏛️","skin_tone_support":false,"name":"classical building","slug":"classical_building"},{"emoji":"🏗️","skin_tone_support":false,"name":"building construction","slug":"building_construction"},{"emoji":"🧱","skin_tone_support":false,"name":"brick","slug":"brick"},{"emoji":"🪨","skin_tone_support":false,"name":"rock","slug":"rock"},{"emoji":"🪵","skin_tone_support":false,"name":"wood","slug":"wood"},{"emoji":"🛖","skin_tone_support":false,"name":"hut","slug":"hut"},{"emoji":"🏘️","skin_tone_support":false,"name":"houses","slug":"houses"},{"emoji":"🏚️","skin_tone_support":false,"name":"derelict house","slug":"derelict_house"},{"emoji":"🏠","skin_tone_support":false,"name":"house","slug":"house"},{"emoji":"🏡","skin_tone_support":false,"name":"house with garden","slug":"house_with_garden"},{"emoji":"🏢","skin_tone_support":false,"name":"office building","slug":"office_building"},{"emoji":"🏣","skin_tone_support":false,"name":"Japanese post office","slug":"japanese_post_office"},{"emoji":"🏤","skin_tone_support":false,"name":"post office","slug":"post_office"},{"emoji":"🏥","skin_tone_support":false,"name":"hospital","slug":"hospital"},{"emoji":"🏦","skin_tone_support":false,"name":"bank","slug":"bank"},{"emoji":"🏨","skin_tone_support":false,"name":"hotel","slug":"hotel"},{"emoji":"🏩","skin_tone_support":false,"name":"love hotel","slug":"love_hotel"},{"emoji":"🏪","skin_tone_support":false,"name":"convenience store","slug":"convenience_store"},{"emoji":"🏫","skin_tone_support":false,"name":"school","slug":"school"},{"emoji":"🏬","skin_tone_support":false,"name":"department store","slug":"department_store"},{"emoji":"🏭","skin_tone_support":false,"name":"factory","slug":"factory"},{"emoji":"🏯","skin_tone_support":false,"name":"Japanese castle","slug":"japanese_castle"},{"emoji":"🏰","skin_tone_support":false,"name":"castle","slug":"castle"},{"emoji":"💒","skin_tone_support":false,"name":"wedding","slug":"wedding"},{"emoji":"🗼","skin_tone_support":false,"name":"Tokyo tower","slug":"tokyo_tower"},{"emoji":"🗽","skin_tone_support":false,"name":"Statue of Liberty","slug":"statue_of_liberty"},{"emoji":"⛪","skin_tone_support":false,"name":"church","slug":"church"},{"emoji":"🕌","skin_tone_support":false,"name":"mosque","slug":"mosque"},{"emoji":"🛕","skin_tone_support":false,"name":"hindu temple","slug":"hindu_temple"},{"emoji":"🕍","skin_tone_support":false,"name":"synagogue","slug":"synagogue"},{"emoji":"⛩️","skin_tone_support":false,"name":"shinto shrine","slug":"shinto_shrine"},{"emoji":"🕋","skin_tone_support":false,"name":"kaaba","slug":"kaaba"},{"emoji":"⛲","skin_tone_support":false,"name":"fountain","slug":"fountain"},{"emoji":"⛺","skin_tone_support":false,"name":"tent","slug":"tent"},{"emoji":"🌁","skin_tone_support":false,"name":"foggy","slug":"foggy"},{"emoji":"🌃","skin_tone_support":false,"name":"night with stars","slug":"night_with_stars"},{"emoji":"🏙️","skin_tone_support":false,"name":"cityscape","slug":"cityscape"},{"emoji":"🌄","skin_tone_support":false,"name":"sunrise over mountains","slug":"sunrise_over_mountains"},{"emoji":"🌅","skin_tone_support":false,"name":"sunrise","slug":"sunrise"},{"emoji":"🌆","skin_tone_support":false,"name":"cityscape at dusk","slug":"cityscape_at_dusk"},{"emoji":"🌇","skin_tone_support":false,"name":"sunset","slug":"sunset"},{"emoji":"🌉","skin_tone_support":false,"name":"bridge at night","slug":"bridge_at_night"},{"emoji":"♨️","skin_tone_support":false,"name":"hot springs","slug":"hot_springs"},{"emoji":"🎠","skin_tone_support":false,"name":"carousel horse","slug":"carousel_horse"},{"emoji":"🛝","skin_tone_support":false,"name":"playground slide","slug":"playground_slide"},{"emoji":"🎡","skin_tone_support":false,"name":"ferris wheel","slug":"ferris_wheel"},{"emoji":"🎢","skin_tone_support":false,"name":"roller coaster","slug":"roller_coaster"},{"emoji":"💈","skin_tone_support":false,"name":"barber pole","slug":"barber_pole"},{"emoji":"🎪","skin_tone_support":false,"name":"circus tent","slug":"circus_tent"},{"emoji":"🚂","skin_tone_support":false,"name":"locomotive","slug":"locomotive"},{"emoji":"🚃","skin_tone_support":false,"name":"railway car","slug":"railway_car"},{"emoji":"🚄","skin_tone_support":false,"name":"high-speed train","slug":"high_speed_train"},{"emoji":"🚅","skin_tone_support":false,"name":"bullet train","slug":"bullet_train"},{"emoji":"🚆","skin_tone_support":false,"name":"train","slug":"train"},{"emoji":"🚇","skin_tone_support":false,"name":"metro","slug":"metro"},{"emoji":"🚈","skin_tone_support":false,"name":"light rail","slug":"light_rail"},{"emoji":"🚉","skin_tone_support":false,"name":"station","slug":"station"},{"emoji":"🚊","skin_tone_support":false,"name":"tram","slug":"tram"},{"emoji":"🚝","skin_tone_support":false,"name":"monorail","slug":"monorail"},{"emoji":"🚞","skin_tone_support":false,"name":"mountain railway","slug":"mountain_railway"},{"emoji":"🚋","skin_tone_support":false,"name":"tram car","slug":"tram_car"},{"emoji":"🚌","skin_tone_support":false,"name":"bus","slug":"bus"},{"emoji":"🚍","skin_tone_support":false,"name":"oncoming bus","slug":"oncoming_bus"},{"emoji":"🚎","skin_tone_support":false,"name":"trolleybus","slug":"trolleybus"},{"emoji":"🚐","skin_tone_support":false,"name":"minibus","slug":"minibus"},{"emoji":"🚑","skin_tone_support":false,"name":"ambulance","slug":"ambulance"},{"emoji":"🚒","skin_tone_support":false,"name":"fire engine","slug":"fire_engine"},{"emoji":"🚓","skin_tone_support":false,"name":"police car","slug":"police_car"},{"emoji":"🚔","skin_tone_support":false,"name":"oncoming police car","slug":"oncoming_police_car"},{"emoji":"🚕","skin_tone_support":false,"name":"taxi","slug":"taxi"},{"emoji":"🚖","skin_tone_support":false,"name":"oncoming taxi","slug":"oncoming_taxi"},{"emoji":"🚗","skin_tone_support":false,"name":"automobile","slug":"automobile"},{"emoji":"🚘","skin_tone_support":false,"name":"oncoming automobile","slug":"oncoming_automobile"},{"emoji":"🚙","skin_tone_support":false,"name":"sport utility vehicle","slug":"sport_utility_vehicle"},{"emoji":"🛻","skin_tone_support":false,"name":"pickup truck","slug":"pickup_truck"},{"emoji":"🚚","skin_tone_support":false,"name":"delivery truck","slug":"delivery_truck"},{"emoji":"🚛","skin_tone_support":false,"name":"articulated lorry","slug":"articulated_lorry"},{"emoji":"🚜","skin_tone_support":false,"name":"tractor","slug":"tractor"},{"emoji":"🏎️","skin_tone_support":false,"name":"racing car","slug":"racing_car"},{"emoji":"🏍️","skin_tone_support":false,"name":"motorcycle","slug":"motorcycle"},{"emoji":"🛵","skin_tone_support":false,"name":"motor scooter","slug":"motor_scooter"},{"emoji":"🦽","skin_tone_support":false,"name":"manual wheelchair","slug":"manual_wheelchair"},{"emoji":"🦼","skin_tone_support":false,"name":"motorized wheelchair","slug":"motorized_wheelchair"},{"emoji":"🛺","skin_tone_support":false,"name":"auto rickshaw","slug":"auto_rickshaw"},{"emoji":"🚲","skin_tone_support":false,"name":"bicycle","slug":"bicycle"},{"emoji":"🛴","skin_tone_support":false,"name":"kick scooter","slug":"kick_scooter"},{"emoji":"🛹","skin_tone_support":false,"name":"skateboard","slug":"skateboard"},{"emoji":"🛼","skin_tone_support":false,"name":"roller skate","slug":"roller_skate"},{"emoji":"🚏","skin_tone_support":false,"name":"bus stop","slug":"bus_stop"},{"emoji":"🛣️","skin_tone_support":false,"name":"motorway","slug":"motorway"},{"emoji":"🛤️","skin_tone_support":false,"name":"railway track","slug":"railway_track"},{"emoji":"🛢️","skin_tone_support":false,"name":"oil drum","slug":"oil_drum"},{"emoji":"⛽","skin_tone_support":false,"name":"fuel pump","slug":"fuel_pump"},{"emoji":"🛞","skin_tone_support":false,"name":"wheel","slug":"wheel"},{"emoji":"🚨","skin_tone_support":false,"name":"police car light","slug":"police_car_light"},{"emoji":"🚥","skin_tone_support":false,"name":"horizontal traffic light","slug":"horizontal_traffic_light"},{"emoji":"🚦","skin_tone_support":false,"name":"vertical traffic light","slug":"vertical_traffic_light"},{"emoji":"🛑","skin_tone_support":false,"name":"stop sign","slug":"stop_sign"},{"emoji":"🚧","skin_tone_support":false,"name":"construction","slug":"construction"},{"emoji":"⚓","skin_tone_support":false,"name":"anchor","slug":"anchor"},{"emoji":"🛟","skin_tone_support":false,"name":"ring buoy","slug":"ring_buoy"},{"emoji":"⛵","skin_tone_support":false,"name":"sailboat","slug":"sailboat"},{"emoji":"🛶","skin_tone_support":false,"name":"canoe","slug":"canoe"},{"emoji":"🚤","skin_tone_support":false,"name":"speedboat","slug":"speedboat"},{"emoji":"🛳️","skin_tone_support":false,"name":"passenger ship","slug":"passenger_ship"},{"emoji":"⛴️","skin_tone_support":false,"name":"ferry","slug":"ferry"},{"emoji":"🛥️","skin_tone_support":false,"name":"motor boat","slug":"motor_boat"},{"emoji":"🚢","skin_tone_support":false,"name":"ship","slug":"ship"},{"emoji":"✈️","skin_tone_support":false,"name":"airplane","slug":"airplane"},{"emoji":"🛩️","skin_tone_support":false,"name":"small airplane","slug":"small_airplane"},{"emoji":"🛫","skin_tone_support":false,"name":"airplane departure","slug":"airplane_departure"},{"emoji":"🛬","skin_tone_support":false,"name":"airplane arrival","slug":"airplane_arrival"},{"emoji":"🪂","skin_tone_support":false,"name":"parachute","slug":"parachute"},{"emoji":"💺","skin_tone_support":false,"name":"seat","slug":"seat"},{"emoji":"🚁","skin_tone_support":false,"name":"helicopter","slug":"helicopter"},{"emoji":"🚟","skin_tone_support":false,"name":"suspension railway","slug":"suspension_railway"},{"emoji":"🚠","skin_tone_support":false,"name":"mountain cableway","slug":"mountain_cableway"},{"emoji":"🚡","skin_tone_support":false,"name":"aerial tramway","slug":"aerial_tramway"},{"emoji":"🛰️","skin_tone_support":false,"name":"satellite","slug":"satellite"},{"emoji":"🚀","skin_tone_support":false,"name":"rocket","slug":"rocket"},{"emoji":"🛸","skin_tone_support":false,"name":"flying saucer","slug":"flying_saucer"},{"emoji":"🛎️","skin_tone_support":false,"name":"bellhop bell","slug":"bellhop_bell"},{"emoji":"🧳","skin_tone_support":false,"name":"luggage","slug":"luggage"},{"emoji":"⌛","skin_tone_support":false,"name":"hourglass done","slug":"hourglass_done"},{"emoji":"⏳","skin_tone_support":false,"name":"hourglass not done","slug":"hourglass_not_done"},{"emoji":"⌚","skin_tone_support":false,"name":"watch","slug":"watch"},{"emoji":"⏰","skin_tone_support":false,"name":"alarm clock","slug":"alarm_clock"},{"emoji":"⏱️","skin_tone_support":false,"name":"stopwatch","slug":"stopwatch"},{"emoji":"⏲️","skin_tone_support":false,"name":"timer clock","slug":"timer_clock"},{"emoji":"🕰️","skin_tone_support":false,"name":"mantelpiece clock","slug":"mantelpiece_clock"},{"emoji":"🕛","skin_tone_support":false,"name":"twelve o’clock","slug":"twelve_o_clock"},{"emoji":"🕧","skin_tone_support":false,"name":"twelve-thirty","slug":"twelve_thirty"},{"emoji":"🕐","skin_tone_support":false,"name":"one o’clock","slug":"one_o_clock"},{"emoji":"🕜","skin_tone_support":false,"name":"one-thirty","slug":"one_thirty"},{"emoji":"🕑","skin_tone_support":false,"name":"two o’clock","slug":"two_o_clock"},{"emoji":"🕝","skin_tone_support":false,"name":"two-thirty","slug":"two_thirty"},{"emoji":"🕒","skin_tone_support":false,"name":"three o’clock","slug":"three_o_clock"},{"emoji":"🕞","skin_tone_support":false,"name":"three-thirty","slug":"three_thirty"},{"emoji":"🕓","skin_tone_support":false,"name":"four o’clock","slug":"four_o_clock"},{"emoji":"🕟","skin_tone_support":false,"name":"four-thirty","slug":"four_thirty"},{"emoji":"🕔","skin_tone_support":false,"name":"five o’clock","slug":"five_o_clock"},{"emoji":"🕠","skin_tone_support":false,"name":"five-thirty","slug":"five_thirty"},{"emoji":"🕕","skin_tone_support":false,"name":"six o’clock","slug":"six_o_clock"},{"emoji":"🕡","skin_tone_support":false,"name":"six-thirty","slug":"six_thirty"},{"emoji":"🕖","skin_tone_support":false,"name":"seven o’clock","slug":"seven_o_clock"},{"emoji":"🕢","skin_tone_support":false,"name":"seven-thirty","slug":"seven_thirty"},{"emoji":"🕗","skin_tone_support":false,"name":"eight o’clock","slug":"eight_o_clock"},{"emoji":"🕣","skin_tone_support":false,"name":"eight-thirty","slug":"eight_thirty"},{"emoji":"🕘","skin_tone_support":false,"name":"nine o’clock","slug":"nine_o_clock"},{"emoji":"🕤","skin_tone_support":false,"name":"nine-thirty","slug":"nine_thirty"},{"emoji":"🕙","skin_tone_support":false,"name":"ten o’clock","slug":"ten_o_clock"},{"emoji":"🕥","skin_tone_support":false,"name":"ten-thirty","slug":"ten_thirty"},{"emoji":"🕚","skin_tone_support":false,"name":"eleven o’clock","slug":"eleven_o_clock"},{"emoji":"🕦","skin_tone_support":false,"name":"eleven-thirty","slug":"eleven_thirty"},{"emoji":"🌑","skin_tone_support":false,"name":"new moon","slug":"new_moon"},{"emoji":"🌒","skin_tone_support":false,"name":"waxing crescent moon","slug":"waxing_crescent_moon"},{"emoji":"🌓","skin_tone_support":false,"name":"first quarter moon","slug":"first_quarter_moon"},{"emoji":"🌔","skin_tone_support":false,"name":"waxing gibbous moon","slug":"waxing_gibbous_moon"},{"emoji":"🌕","skin_tone_support":false,"name":"full moon","slug":"full_moon"},{"emoji":"🌖","skin_tone_support":false,"name":"waning gibbous moon","slug":"waning_gibbous_moon"},{"emoji":"🌗","skin_tone_support":false,"name":"last quarter moon","slug":"last_quarter_moon"},{"emoji":"🌘","skin_tone_support":false,"name":"waning crescent moon","slug":"waning_crescent_moon"},{"emoji":"🌙","skin_tone_support":false,"name":"crescent moon","slug":"crescent_moon"},{"emoji":"🌚","skin_tone_support":false,"name":"new moon face","slug":"new_moon_face"},{"emoji":"🌛","skin_tone_support":false,"name":"first quarter moon face","slug":"first_quarter_moon_face"},{"emoji":"🌜","skin_tone_support":false,"name":"last quarter moon face","slug":"last_quarter_moon_face"},{"emoji":"🌡️","skin_tone_support":false,"name":"thermometer","slug":"thermometer"},{"emoji":"☀️","skin_tone_support":false,"name":"sun","slug":"sun"},{"emoji":"🌝","skin_tone_support":false,"name":"full moon face","slug":"full_moon_face"},{"emoji":"🌞","skin_tone_support":false,"name":"sun with face","slug":"sun_with_face"},{"emoji":"🪐","skin_tone_support":false,"name":"ringed planet","slug":"ringed_planet"},{"emoji":"⭐","skin_tone_support":false,"name":"star","slug":"star"},{"emoji":"🌟","skin_tone_support":false,"name":"glowing star","slug":"glowing_star"},{"emoji":"🌠","skin_tone_support":false,"name":"shooting star","slug":"shooting_star"},{"emoji":"🌌","skin_tone_support":false,"name":"milky way","slug":"milky_way"},{"emoji":"☁️","skin_tone_support":false,"name":"cloud","slug":"cloud"},{"emoji":"⛅","skin_tone_support":false,"name":"sun behind cloud","slug":"sun_behind_cloud"},{"emoji":"⛈️","skin_tone_support":false,"name":"cloud with lightning and rain","slug":"cloud_with_lightning_and_rain"},{"emoji":"🌤️","skin_tone_support":false,"name":"sun behind small cloud","slug":"sun_behind_small_cloud"},{"emoji":"🌥️","skin_tone_support":false,"name":"sun behind large cloud","slug":"sun_behind_large_cloud"},{"emoji":"🌦️","skin_tone_support":false,"name":"sun behind rain cloud","slug":"sun_behind_rain_cloud"},{"emoji":"🌧️","skin_tone_support":false,"name":"cloud with rain","slug":"cloud_with_rain"},{"emoji":"🌨️","skin_tone_support":false,"name":"cloud with snow","slug":"cloud_with_snow"},{"emoji":"🌩️","skin_tone_support":false,"name":"cloud with lightning","slug":"cloud_with_lightning"},{"emoji":"🌪️","skin_tone_support":false,"name":"tornado","slug":"tornado"},{"emoji":"🌫️","skin_tone_support":false,"name":"fog","slug":"fog"},{"emoji":"🌬️","skin_tone_support":false,"name":"wind face","slug":"wind_face"},{"emoji":"🌀","skin_tone_support":false,"name":"cyclone","slug":"cyclone"},{"emoji":"🌈","skin_tone_support":false,"name":"rainbow","slug":"rainbow"},{"emoji":"🌂","skin_tone_support":false,"name":"closed umbrella","slug":"closed_umbrella"},{"emoji":"☂️","skin_tone_support":false,"name":"umbrella","slug":"umbrella"},{"emoji":"☔","skin_tone_support":false,"name":"umbrella with rain drops","slug":"umbrella_with_rain_drops"},{"emoji":"⛱️","skin_tone_support":false,"name":"umbrella on ground","slug":"umbrella_on_ground"},{"emoji":"⚡","skin_tone_support":false,"name":"high voltage","slug":"high_voltage"},{"emoji":"❄️","skin_tone_support":false,"name":"snowflake","slug":"snowflake"},{"emoji":"☃️","skin_tone_support":false,"name":"snowman","slug":"snowman"},{"emoji":"⛄","skin_tone_support":false,"name":"snowman without snow","slug":"snowman_without_snow"},{"emoji":"☄️","skin_tone_support":false,"name":"comet","slug":"comet"},{"emoji":"🔥","skin_tone_support":false,"name":"fire","slug":"fire"},{"emoji":"💧","skin_tone_support":false,"name":"droplet","slug":"droplet"},{"emoji":"🌊","skin_tone_support":false,"name":"water wave","slug":"water_wave"}],"activities":[{"emoji":"🎃","skin_tone_support":false,"name":"jack-o-lantern","slug":"jack_o_lantern"},{"emoji":"🎄","skin_tone_support":false,"name":"Christmas tree","slug":"christmas_tree"},{"emoji":"🎆","skin_tone_support":false,"name":"fireworks","slug":"fireworks"},{"emoji":"🎇","skin_tone_support":false,"name":"sparkler","slug":"sparkler"},{"emoji":"🧨","skin_tone_support":false,"name":"firecracker","slug":"firecracker"},{"emoji":"✨","skin_tone_support":false,"name":"sparkles","slug":"sparkles"},{"emoji":"🎈","skin_tone_support":false,"name":"balloon","slug":"balloon"},{"emoji":"🎉","skin_tone_support":false,"name":"party popper","slug":"party_popper"},{"emoji":"🎊","skin_tone_support":false,"name":"confetti ball","slug":"confetti_ball"},{"emoji":"🎋","skin_tone_support":false,"name":"tanabata tree","slug":"tanabata_tree"},{"emoji":"🎍","skin_tone_support":false,"name":"pine decoration","slug":"pine_decoration"},{"emoji":"🎎","skin_tone_support":false,"name":"Japanese dolls","slug":"japanese_dolls"},{"emoji":"🎏","skin_tone_support":false,"name":"carp streamer","slug":"carp_streamer"},{"emoji":"🎐","skin_tone_support":false,"name":"wind chime","slug":"wind_chime"},{"emoji":"🎑","skin_tone_support":false,"name":"moon viewing ceremony","slug":"moon_viewing_ceremony"},{"emoji":"🧧","skin_tone_support":false,"name":"red envelope","slug":"red_envelope"},{"emoji":"🎀","skin_tone_support":false,"name":"ribbon","slug":"ribbon"},{"emoji":"🎁","skin_tone_support":false,"name":"wrapped gift","slug":"wrapped_gift"},{"emoji":"🎗️","skin_tone_support":false,"name":"reminder ribbon","slug":"reminder_ribbon"},{"emoji":"🎟️","skin_tone_support":false,"name":"admission tickets","slug":"admission_tickets"},{"emoji":"🎫","skin_tone_support":false,"name":"ticket","slug":"ticket"},{"emoji":"🎖️","skin_tone_support":false,"name":"military medal","slug":"military_medal"},{"emoji":"🏆","skin_tone_support":false,"name":"trophy","slug":"trophy"},{"emoji":"🏅","skin_tone_support":false,"name":"sports medal","slug":"sports_medal"},{"emoji":"🥇","skin_tone_support":false,"name":"1st place medal","slug":"1st_place_medal"},{"emoji":"🥈","skin_tone_support":false,"name":"2nd place medal","slug":"2nd_place_medal"},{"emoji":"🥉","skin_tone_support":false,"name":"3rd place medal","slug":"3rd_place_medal"},{"emoji":"⚽","skin_tone_support":false,"name":"soccer ball","slug":"soccer_ball"},{"emoji":"⚾","skin_tone_support":false,"name":"baseball","slug":"baseball"},{"emoji":"🥎","skin_tone_support":false,"name":"softball","slug":"softball"},{"emoji":"🏀","skin_tone_support":false,"name":"basketball","slug":"basketball"},{"emoji":"🏐","skin_tone_support":false,"name":"volleyball","slug":"volleyball"},{"emoji":"🏈","skin_tone_support":false,"name":"american football","slug":"american_football"},{"emoji":"🏉","skin_tone_support":false,"name":"rugby football","slug":"rugby_football"},{"emoji":"🎾","skin_tone_support":false,"name":"tennis","slug":"tennis"},{"emoji":"🥏","skin_tone_support":false,"name":"flying disc","slug":"flying_disc"},{"emoji":"🎳","skin_tone_support":false,"name":"bowling","slug":"bowling"},{"emoji":"🏏","skin_tone_support":false,"name":"cricket game","slug":"cricket_game"},{"emoji":"🏑","skin_tone_support":false,"name":"field hockey","slug":"field_hockey"},{"emoji":"🏒","skin_tone_support":false,"name":"ice hockey","slug":"ice_hockey"},{"emoji":"🥍","skin_tone_support":false,"name":"lacrosse","slug":"lacrosse"},{"emoji":"🏓","skin_tone_support":false,"name":"ping pong","slug":"ping_pong"},{"emoji":"🏸","skin_tone_support":false,"name":"badminton","slug":"badminton"},{"emoji":"🥊","skin_tone_support":false,"name":"boxing glove","slug":"boxing_glove"},{"emoji":"🥋","skin_tone_support":false,"name":"martial arts uniform","slug":"martial_arts_uniform"},{"emoji":"🥅","skin_tone_support":false,"name":"goal net","slug":"goal_net"},{"emoji":"⛳","skin_tone_support":false,"name":"flag in hole","slug":"flag_in_hole"},{"emoji":"⛸️","skin_tone_support":false,"name":"ice skate","slug":"ice_skate"},{"emoji":"🎣","skin_tone_support":false,"name":"fishing pole","slug":"fishing_pole"},{"emoji":"🤿","skin_tone_support":false,"name":"diving mask","slug":"diving_mask"},{"emoji":"🎽","skin_tone_support":false,"name":"running shirt","slug":"running_shirt"},{"emoji":"🎿","skin_tone_support":false,"name":"skis","slug":"skis"},{"emoji":"🛷","skin_tone_support":false,"name":"sled","slug":"sled"},{"emoji":"🥌","skin_tone_support":false,"name":"curling stone","slug":"curling_stone"},{"emoji":"🎯","skin_tone_support":false,"name":"bullseye","slug":"bullseye"},{"emoji":"🪀","skin_tone_support":false,"name":"yo-yo","slug":"yo_yo"},{"emoji":"🪁","skin_tone_support":false,"name":"kite","slug":"kite"},{"emoji":"🎱","skin_tone_support":false,"name":"pool 8 ball","slug":"pool_8_ball"},{"emoji":"🔮","skin_tone_support":false,"name":"crystal ball","slug":"crystal_ball"},{"emoji":"🪄","skin_tone_support":false,"name":"magic wand","slug":"magic_wand"},{"emoji":"🧿","skin_tone_support":false,"name":"nazar amulet","slug":"nazar_amulet"},{"emoji":"🪬","skin_tone_support":false,"name":"hamsa","slug":"hamsa"},{"emoji":"🎮","skin_tone_support":false,"name":"video game","slug":"video_game"},{"emoji":"🕹️","skin_tone_support":false,"name":"joystick","slug":"joystick"},{"emoji":"🎰","skin_tone_support":false,"name":"slot machine","slug":"slot_machine"},{"emoji":"🎲","skin_tone_support":false,"name":"game die","slug":"game_die"},{"emoji":"🧩","skin_tone_support":false,"name":"puzzle piece","slug":"puzzle_piece"},{"emoji":"🧸","skin_tone_support":false,"name":"teddy bear","slug":"teddy_bear"},{"emoji":"🪅","skin_tone_support":false,"name":"piñata","slug":"pinata"},{"emoji":"🪩","skin_tone_support":false,"name":"mirror ball","slug":"mirror_ball"},{"emoji":"🪆","skin_tone_support":false,"name":"nesting dolls","slug":"nesting_dolls"},{"emoji":"♠️","skin_tone_support":false,"name":"spade suit","slug":"spade_suit"},{"emoji":"♥️","skin_tone_support":false,"name":"heart suit","slug":"heart_suit"},{"emoji":"♦️","skin_tone_support":false,"name":"diamond suit","slug":"diamond_suit"},{"emoji":"♣️","skin_tone_support":false,"name":"club suit","slug":"club_suit"},{"emoji":"♟️","skin_tone_support":false,"name":"chess pawn","slug":"chess_pawn"},{"emoji":"🃏","skin_tone_support":false,"name":"joker","slug":"joker"},{"emoji":"🀄","skin_tone_support":false,"name":"mahjong red dragon","slug":"mahjong_red_dragon"},{"emoji":"🎴","skin_tone_support":false,"name":"flower playing cards","slug":"flower_playing_cards"},{"emoji":"🎭","skin_tone_support":false,"name":"performing arts","slug":"performing_arts"},{"emoji":"🖼️","skin_tone_support":false,"name":"framed picture","slug":"framed_picture"},{"emoji":"🎨","skin_tone_support":false,"name":"artist palette","slug":"artist_palette"},{"emoji":"🧵","skin_tone_support":false,"name":"thread","slug":"thread"},{"emoji":"🪡","skin_tone_support":false,"name":"sewing needle","slug":"sewing_needle"},{"emoji":"🧶","skin_tone_support":false,"name":"yarn","slug":"yarn"},{"emoji":"🪢","skin_tone_support":false,"name":"knot","slug":"knot"}],"objects":[{"emoji":"👓","skin_tone_support":false,"name":"glasses","slug":"glasses"},{"emoji":"🕶️","skin_tone_support":false,"name":"sunglasses","slug":"sunglasses"},{"emoji":"🥽","skin_tone_support":false,"name":"goggles","slug":"goggles"},{"emoji":"🥼","skin_tone_support":false,"name":"lab coat","slug":"lab_coat"},{"emoji":"🦺","skin_tone_support":false,"name":"safety vest","slug":"safety_vest"},{"emoji":"👔","skin_tone_support":false,"name":"necktie","slug":"necktie"},{"emoji":"👕","skin_tone_support":false,"name":"t-shirt","slug":"t_shirt"},{"emoji":"👖","skin_tone_support":false,"name":"jeans","slug":"jeans"},{"emoji":"🧣","skin_tone_support":false,"name":"scarf","slug":"scarf"},{"emoji":"🧤","skin_tone_support":false,"name":"gloves","slug":"gloves"},{"emoji":"🧥","skin_tone_support":false,"name":"coat","slug":"coat"},{"emoji":"🧦","skin_tone_support":false,"name":"socks","slug":"socks"},{"emoji":"👗","skin_tone_support":false,"name":"dress","slug":"dress"},{"emoji":"👘","skin_tone_support":false,"name":"kimono","slug":"kimono"},{"emoji":"🥻","skin_tone_support":false,"name":"sari","slug":"sari"},{"emoji":"🩱","skin_tone_support":false,"name":"one-piece swimsuit","slug":"one_piece_swimsuit"},{"emoji":"🩲","skin_tone_support":false,"name":"briefs","slug":"briefs"},{"emoji":"🩳","skin_tone_support":false,"name":"shorts","slug":"shorts"},{"emoji":"👙","skin_tone_support":false,"name":"bikini","slug":"bikini"},{"emoji":"👚","skin_tone_support":false,"name":"woman’s clothes","slug":"woman_s_clothes"},{"emoji":"👛","skin_tone_support":false,"name":"purse","slug":"purse"},{"emoji":"👜","skin_tone_support":false,"name":"handbag","slug":"handbag"},{"emoji":"👝","skin_tone_support":false,"name":"clutch bag","slug":"clutch_bag"},{"emoji":"🛍️","skin_tone_support":false,"name":"shopping bags","slug":"shopping_bags"},{"emoji":"🎒","skin_tone_support":false,"name":"backpack","slug":"backpack"},{"emoji":"🩴","skin_tone_support":false,"name":"thong sandal","slug":"thong_sandal"},{"emoji":"👞","skin_tone_support":false,"name":"man’s shoe","slug":"man_s_shoe"},{"emoji":"👟","skin_tone_support":false,"name":"running shoe","slug":"running_shoe"},{"emoji":"🥾","skin_tone_support":false,"name":"hiking boot","slug":"hiking_boot"},{"emoji":"🥿","skin_tone_support":false,"name":"flat shoe","slug":"flat_shoe"},{"emoji":"👠","skin_tone_support":false,"name":"high-heeled shoe","slug":"high_heeled_shoe"},{"emoji":"👡","skin_tone_support":false,"name":"woman’s sandal","slug":"woman_s_sandal"},{"emoji":"🩰","skin_tone_support":false,"name":"ballet shoes","slug":"ballet_shoes"},{"emoji":"👢","skin_tone_support":false,"name":"woman’s boot","slug":"woman_s_boot"},{"emoji":"👑","skin_tone_support":false,"name":"crown","slug":"crown"},{"emoji":"👒","skin_tone_support":false,"name":"woman’s hat","slug":"woman_s_hat"},{"emoji":"🎩","skin_tone_support":false,"name":"top hat","slug":"top_hat"},{"emoji":"🎓","skin_tone_support":false,"name":"graduation cap","slug":"graduation_cap"},{"emoji":"🧢","skin_tone_support":false,"name":"billed cap","slug":"billed_cap"},{"emoji":"🪖","skin_tone_support":false,"name":"military helmet","slug":"military_helmet"},{"emoji":"⛑️","skin_tone_support":false,"name":"rescue worker’s helmet","slug":"rescue_worker_s_helmet"},{"emoji":"📿","skin_tone_support":false,"name":"prayer beads","slug":"prayer_beads"},{"emoji":"💄","skin_tone_support":false,"name":"lipstick","slug":"lipstick"},{"emoji":"💍","skin_tone_support":false,"name":"ring","slug":"ring"},{"emoji":"💎","skin_tone_support":false,"name":"gem stone","slug":"gem_stone"},{"emoji":"🔇","skin_tone_support":false,"name":"muted speaker","slug":"muted_speaker"},{"emoji":"🔈","skin_tone_support":false,"name":"speaker low volume","slug":"speaker_low_volume"},{"emoji":"🔉","skin_tone_support":false,"name":"speaker medium volume","slug":"speaker_medium_volume"},{"emoji":"🔊","skin_tone_support":false,"name":"speaker high volume","slug":"speaker_high_volume"},{"emoji":"📢","skin_tone_support":false,"name":"loudspeaker","slug":"loudspeaker"},{"emoji":"📣","skin_tone_support":false,"name":"megaphone","slug":"megaphone"},{"emoji":"📯","skin_tone_support":false,"name":"postal horn","slug":"postal_horn"},{"emoji":"🔔","skin_tone_support":false,"name":"bell","slug":"bell"},{"emoji":"🔕","skin_tone_support":false,"name":"bell with slash","slug":"bell_with_slash"},{"emoji":"🎼","skin_tone_support":false,"name":"musical score","slug":"musical_score"},{"emoji":"🎵","skin_tone_support":false,"name":"musical note","slug":"musical_note"},{"emoji":"🎶","skin_tone_support":false,"name":"musical notes","slug":"musical_notes"},{"emoji":"🎙️","skin_tone_support":false,"name":"studio microphone","slug":"studio_microphone"},{"emoji":"🎚️","skin_tone_support":false,"name":"level slider","slug":"level_slider"},{"emoji":"🎛️","skin_tone_support":false,"name":"control knobs","slug":"control_knobs"},{"emoji":"🎤","skin_tone_support":false,"name":"microphone","slug":"microphone"},{"emoji":"🎧","skin_tone_support":false,"name":"headphone","slug":"headphone"},{"emoji":"📻","skin_tone_support":false,"name":"radio","slug":"radio"},{"emoji":"🎷","skin_tone_support":false,"name":"saxophone","slug":"saxophone"},{"emoji":"🪗","skin_tone_support":false,"name":"accordion","slug":"accordion"},{"emoji":"🎸","skin_tone_support":false,"name":"guitar","slug":"guitar"},{"emoji":"🎹","skin_tone_support":false,"name":"musical keyboard","slug":"musical_keyboard"},{"emoji":"🎺","skin_tone_support":false,"name":"trumpet","slug":"trumpet"},{"emoji":"🎻","skin_tone_support":false,"name":"violin","slug":"violin"},{"emoji":"🪕","skin_tone_support":false,"name":"banjo","slug":"banjo"},{"emoji":"🥁","skin_tone_support":false,"name":"drum","slug":"drum"},{"emoji":"🪘","skin_tone_support":false,"name":"long drum","slug":"long_drum"},{"emoji":"📱","skin_tone_support":false,"name":"mobile phone","slug":"mobile_phone"},{"emoji":"📲","skin_tone_support":false,"name":"mobile phone with arrow","slug":"mobile_phone_with_arrow"},{"emoji":"☎️","skin_tone_support":false,"name":"telephone","slug":"telephone"},{"emoji":"📞","skin_tone_support":false,"name":"telephone receiver","slug":"telephone_receiver"},{"emoji":"📟","skin_tone_support":false,"name":"pager","slug":"pager"},{"emoji":"📠","skin_tone_support":false,"name":"fax machine","slug":"fax_machine"},{"emoji":"🔋","skin_tone_support":false,"name":"battery","slug":"battery"},{"emoji":"🪫","skin_tone_support":false,"name":"low battery","slug":"low_battery"},{"emoji":"🔌","skin_tone_support":false,"name":"electric plug","slug":"electric_plug"},{"emoji":"💻","skin_tone_support":false,"name":"laptop","slug":"laptop"},{"emoji":"🖥️","skin_tone_support":false,"name":"desktop computer","slug":"desktop_computer"},{"emoji":"🖨️","skin_tone_support":false,"name":"printer","slug":"printer"},{"emoji":"⌨️","skin_tone_support":false,"name":"keyboard","slug":"keyboard"},{"emoji":"🖱️","skin_tone_support":false,"name":"computer mouse","slug":"computer_mouse"},{"emoji":"🖲️","skin_tone_support":false,"name":"trackball","slug":"trackball"},{"emoji":"💽","skin_tone_support":false,"name":"computer disk","slug":"computer_disk"},{"emoji":"💾","skin_tone_support":false,"name":"floppy disk","slug":"floppy_disk"},{"emoji":"💿","skin_tone_support":false,"name":"optical disk","slug":"optical_disk"},{"emoji":"📀","skin_tone_support":false,"name":"dvd","slug":"dvd"},{"emoji":"🧮","skin_tone_support":false,"name":"abacus","slug":"abacus"},{"emoji":"🎥","skin_tone_support":false,"name":"movie camera","slug":"movie_camera"},{"emoji":"🎞️","skin_tone_support":false,"name":"film frames","slug":"film_frames"},{"emoji":"📽️","skin_tone_support":false,"name":"film projector","slug":"film_projector"},{"emoji":"🎬","skin_tone_support":false,"name":"clapper board","slug":"clapper_board"},{"emoji":"📺","skin_tone_support":false,"name":"television","slug":"television"},{"emoji":"📷","skin_tone_support":false,"name":"camera","slug":"camera"},{"emoji":"📸","skin_tone_support":false,"name":"camera with flash","slug":"camera_with_flash"},{"emoji":"📹","skin_tone_support":false,"name":"video camera","slug":"video_camera"},{"emoji":"📼","skin_tone_support":false,"name":"videocassette","slug":"videocassette"},{"emoji":"🔍","skin_tone_support":false,"name":"magnifying glass tilted left","slug":"magnifying_glass_tilted_left"},{"emoji":"🔎","skin_tone_support":false,"name":"magnifying glass tilted right","slug":"magnifying_glass_tilted_right"},{"emoji":"🕯️","skin_tone_support":false,"name":"candle","slug":"candle"},{"emoji":"💡","skin_tone_support":false,"name":"light bulb","slug":"light_bulb"},{"emoji":"🔦","skin_tone_support":false,"name":"flashlight","slug":"flashlight"},{"emoji":"🏮","skin_tone_support":false,"name":"red paper lantern","slug":"red_paper_lantern"},{"emoji":"🪔","skin_tone_support":false,"name":"diya lamp","slug":"diya_lamp"},{"emoji":"📔","skin_tone_support":false,"name":"notebook with decorative cover","slug":"notebook_with_decorative_cover"},{"emoji":"📕","skin_tone_support":false,"name":"closed book","slug":"closed_book"},{"emoji":"📖","skin_tone_support":false,"name":"open book","slug":"open_book"},{"emoji":"📗","skin_tone_support":false,"name":"green book","slug":"green_book"},{"emoji":"📘","skin_tone_support":false,"name":"blue book","slug":"blue_book"},{"emoji":"📙","skin_tone_support":false,"name":"orange book","slug":"orange_book"},{"emoji":"📚","skin_tone_support":false,"name":"books","slug":"books"},{"emoji":"📓","skin_tone_support":false,"name":"notebook","slug":"notebook"},{"emoji":"📒","skin_tone_support":false,"name":"ledger","slug":"ledger"},{"emoji":"📃","skin_tone_support":false,"name":"page with curl","slug":"page_with_curl"},{"emoji":"📜","skin_tone_support":false,"name":"scroll","slug":"scroll"},{"emoji":"📄","skin_tone_support":false,"name":"page facing up","slug":"page_facing_up"},{"emoji":"📰","skin_tone_support":false,"name":"newspaper","slug":"newspaper"},{"emoji":"🗞️","skin_tone_support":false,"name":"rolled-up newspaper","slug":"rolled_up_newspaper"},{"emoji":"📑","skin_tone_support":false,"name":"bookmark tabs","slug":"bookmark_tabs"},{"emoji":"🔖","skin_tone_support":false,"name":"bookmark","slug":"bookmark"},{"emoji":"🏷️","skin_tone_support":false,"name":"label","slug":"label"},{"emoji":"💰","skin_tone_support":false,"name":"money bag","slug":"money_bag"},{"emoji":"🪙","skin_tone_support":false,"name":"coin","slug":"coin"},{"emoji":"💴","skin_tone_support":false,"name":"yen banknote","slug":"yen_banknote"},{"emoji":"💵","skin_tone_support":false,"name":"dollar banknote","slug":"dollar_banknote"},{"emoji":"💶","skin_tone_support":false,"name":"euro banknote","slug":"euro_banknote"},{"emoji":"💷","skin_tone_support":false,"name":"pound banknote","slug":"pound_banknote"},{"emoji":"💸","skin_tone_support":false,"name":"money with wings","slug":"money_with_wings"},{"emoji":"💳","skin_tone_support":false,"name":"credit card","slug":"credit_card"},{"emoji":"🧾","skin_tone_support":false,"name":"receipt","slug":"receipt"},{"emoji":"💹","skin_tone_support":false,"name":"chart increasing with yen","slug":"chart_increasing_with_yen"},{"emoji":"✉️","skin_tone_support":false,"name":"envelope","slug":"envelope"},{"emoji":"📧","skin_tone_support":false,"name":"e-mail","slug":"e_mail"},{"emoji":"📨","skin_tone_support":false,"name":"incoming envelope","slug":"incoming_envelope"},{"emoji":"📩","skin_tone_support":false,"name":"envelope with arrow","slug":"envelope_with_arrow"},{"emoji":"📤","skin_tone_support":false,"name":"outbox tray","slug":"outbox_tray"},{"emoji":"📥","skin_tone_support":false,"name":"inbox tray","slug":"inbox_tray"},{"emoji":"📦","skin_tone_support":false,"name":"package","slug":"package"},{"emoji":"📫","skin_tone_support":false,"name":"closed mailbox with raised flag","slug":"closed_mailbox_with_raised_flag"},{"emoji":"📪","skin_tone_support":false,"name":"closed mailbox with lowered flag","slug":"closed_mailbox_with_lowered_flag"},{"emoji":"📬","skin_tone_support":false,"name":"open mailbox with raised flag","slug":"open_mailbox_with_raised_flag"},{"emoji":"📭","skin_tone_support":false,"name":"open mailbox with lowered flag","slug":"open_mailbox_with_lowered_flag"},{"emoji":"📮","skin_tone_support":false,"name":"postbox","slug":"postbox"},{"emoji":"🗳️","skin_tone_support":false,"name":"ballot box with ballot","slug":"ballot_box_with_ballot"},{"emoji":"✏️","skin_tone_support":false,"name":"pencil","slug":"pencil"},{"emoji":"✒️","skin_tone_support":false,"name":"black nib","slug":"black_nib"},{"emoji":"🖋️","skin_tone_support":false,"name":"fountain pen","slug":"fountain_pen"},{"emoji":"🖊️","skin_tone_support":false,"name":"pen","slug":"pen"},{"emoji":"🖌️","skin_tone_support":false,"name":"paintbrush","slug":"paintbrush"},{"emoji":"🖍️","skin_tone_support":false,"name":"crayon","slug":"crayon"},{"emoji":"📝","skin_tone_support":false,"name":"memo","slug":"memo"},{"emoji":"💼","skin_tone_support":false,"name":"briefcase","slug":"briefcase"},{"emoji":"📁","skin_tone_support":false,"name":"file folder","slug":"file_folder"},{"emoji":"📂","skin_tone_support":false,"name":"open file folder","slug":"open_file_folder"},{"emoji":"🗂️","skin_tone_support":false,"name":"card index dividers","slug":"card_index_dividers"},{"emoji":"📅","skin_tone_support":false,"name":"calendar","slug":"calendar"},{"emoji":"📆","skin_tone_support":false,"name":"tear-off calendar","slug":"tear_off_calendar"},{"emoji":"🗒️","skin_tone_support":false,"name":"spiral notepad","slug":"spiral_notepad"},{"emoji":"🗓️","skin_tone_support":false,"name":"spiral calendar","slug":"spiral_calendar"},{"emoji":"📇","skin_tone_support":false,"name":"card index","slug":"card_index"},{"emoji":"📈","skin_tone_support":false,"name":"chart increasing","slug":"chart_increasing"},{"emoji":"📉","skin_tone_support":false,"name":"chart decreasing","slug":"chart_decreasing"},{"emoji":"📊","skin_tone_support":false,"name":"bar chart","slug":"bar_chart"},{"emoji":"📋","skin_tone_support":false,"name":"clipboard","slug":"clipboard"},{"emoji":"📌","skin_tone_support":false,"name":"pushpin","slug":"pushpin"},{"emoji":"📍","skin_tone_support":false,"name":"round pushpin","slug":"round_pushpin"},{"emoji":"📎","skin_tone_support":false,"name":"paperclip","slug":"paperclip"},{"emoji":"🖇️","skin_tone_support":false,"name":"linked paperclips","slug":"linked_paperclips"},{"emoji":"📏","skin_tone_support":false,"name":"straight ruler","slug":"straight_ruler"},{"emoji":"📐","skin_tone_support":false,"name":"triangular ruler","slug":"triangular_ruler"},{"emoji":"✂️","skin_tone_support":false,"name":"scissors","slug":"scissors"},{"emoji":"🗃️","skin_tone_support":false,"name":"card file box","slug":"card_file_box"},{"emoji":"🗄️","skin_tone_support":false,"name":"file cabinet","slug":"file_cabinet"},{"emoji":"🗑️","skin_tone_support":false,"name":"wastebasket","slug":"wastebasket"},{"emoji":"🔒","skin_tone_support":false,"name":"locked","slug":"locked"},{"emoji":"🔓","skin_tone_support":false,"name":"unlocked","slug":"unlocked"},{"emoji":"🔏","skin_tone_support":false,"name":"locked with pen","slug":"locked_with_pen"},{"emoji":"🔐","skin_tone_support":false,"name":"locked with key","slug":"locked_with_key"},{"emoji":"🔑","skin_tone_support":false,"name":"key","slug":"key"},{"emoji":"🗝️","skin_tone_support":false,"name":"old key","slug":"old_key"},{"emoji":"🔨","skin_tone_support":false,"name":"hammer","slug":"hammer"},{"emoji":"🪓","skin_tone_support":false,"name":"axe","slug":"axe"},{"emoji":"⛏️","skin_tone_support":false,"name":"pick","slug":"pick"},{"emoji":"⚒️","skin_tone_support":false,"name":"hammer and pick","slug":"hammer_and_pick"},{"emoji":"🛠️","skin_tone_support":false,"name":"hammer and wrench","slug":"hammer_and_wrench"},{"emoji":"🗡️","skin_tone_support":false,"name":"dagger","slug":"dagger"},{"emoji":"⚔️","skin_tone_support":false,"name":"crossed swords","slug":"crossed_swords"},{"emoji":"🔫","skin_tone_support":false,"name":"water pistol","slug":"water_pistol"},{"emoji":"🪃","skin_tone_support":false,"name":"boomerang","slug":"boomerang"},{"emoji":"🏹","skin_tone_support":false,"name":"bow and arrow","slug":"bow_and_arrow"},{"emoji":"🛡️","skin_tone_support":false,"name":"shield","slug":"shield"},{"emoji":"🪚","skin_tone_support":false,"name":"carpentry saw","slug":"carpentry_saw"},{"emoji":"🔧","skin_tone_support":false,"name":"wrench","slug":"wrench"},{"emoji":"🪛","skin_tone_support":false,"name":"screwdriver","slug":"screwdriver"},{"emoji":"🔩","skin_tone_support":false,"name":"nut and bolt","slug":"nut_and_bolt"},{"emoji":"⚙️","skin_tone_support":false,"name":"gear","slug":"gear"},{"emoji":"🗜️","skin_tone_support":false,"name":"clamp","slug":"clamp"},{"emoji":"⚖️","skin_tone_support":false,"name":"balance scale","slug":"balance_scale"},{"emoji":"🦯","skin_tone_support":false,"name":"white cane","slug":"white_cane"},{"emoji":"🔗","skin_tone_support":false,"name":"link","slug":"link"},{"emoji":"⛓️","skin_tone_support":false,"name":"chains","slug":"chains"},{"emoji":"🪝","skin_tone_support":false,"name":"hook","slug":"hook"},{"emoji":"🧰","skin_tone_support":false,"name":"toolbox","slug":"toolbox"},{"emoji":"🧲","skin_tone_support":false,"name":"magnet","slug":"magnet"},{"emoji":"🪜","skin_tone_support":false,"name":"ladder","slug":"ladder"},{"emoji":"⚗️","skin_tone_support":false,"name":"alembic","slug":"alembic"},{"emoji":"🧪","skin_tone_support":false,"name":"test tube","slug":"test_tube"},{"emoji":"🧫","skin_tone_support":false,"name":"petri dish","slug":"petri_dish"},{"emoji":"🧬","skin_tone_support":false,"name":"dna","slug":"dna"},{"emoji":"🔬","skin_tone_support":false,"name":"microscope","slug":"microscope"},{"emoji":"🔭","skin_tone_support":false,"name":"telescope","slug":"telescope"},{"emoji":"📡","skin_tone_support":false,"name":"satellite antenna","slug":"satellite_antenna"},{"emoji":"💉","skin_tone_support":false,"name":"syringe","slug":"syringe"},{"emoji":"🩸","skin_tone_support":false,"name":"drop of blood","slug":"drop_of_blood"},{"emoji":"💊","skin_tone_support":false,"name":"pill","slug":"pill"},{"emoji":"🩹","skin_tone_support":false,"name":"adhesive bandage","slug":"adhesive_bandage"},{"emoji":"🩼","skin_tone_support":false,"name":"crutch","slug":"crutch"},{"emoji":"🩺","skin_tone_support":false,"name":"stethoscope","slug":"stethoscope"},{"emoji":"🩻","skin_tone_support":false,"name":"x-ray","slug":"x_ray"},{"emoji":"🚪","skin_tone_support":false,"name":"door","slug":"door"},{"emoji":"🛗","skin_tone_support":false,"name":"elevator","slug":"elevator"},{"emoji":"🪞","skin_tone_support":false,"name":"mirror","slug":"mirror"},{"emoji":"🪟","skin_tone_support":false,"name":"window","slug":"window"},{"emoji":"🛏️","skin_tone_support":false,"name":"bed","slug":"bed"},{"emoji":"🛋️","skin_tone_support":false,"name":"couch and lamp","slug":"couch_and_lamp"},{"emoji":"🪑","skin_tone_support":false,"name":"chair","slug":"chair"},{"emoji":"🚽","skin_tone_support":false,"name":"toilet","slug":"toilet"},{"emoji":"🪠","skin_tone_support":false,"name":"plunger","slug":"plunger"},{"emoji":"🚿","skin_tone_support":false,"name":"shower","slug":"shower"},{"emoji":"🛁","skin_tone_support":false,"name":"bathtub","slug":"bathtub"},{"emoji":"🪤","skin_tone_support":false,"name":"mouse trap","slug":"mouse_trap"},{"emoji":"🪒","skin_tone_support":false,"name":"razor","slug":"razor"},{"emoji":"🧴","skin_tone_support":false,"name":"lotion bottle","slug":"lotion_bottle"},{"emoji":"🧷","skin_tone_support":false,"name":"safety pin","slug":"safety_pin"},{"emoji":"🧹","skin_tone_support":false,"name":"broom","slug":"broom"},{"emoji":"🧺","skin_tone_support":false,"name":"basket","slug":"basket"},{"emoji":"🧻","skin_tone_support":false,"name":"roll of paper","slug":"roll_of_paper"},{"emoji":"🪣","skin_tone_support":false,"name":"bucket","slug":"bucket"},{"emoji":"🧼","skin_tone_support":false,"name":"soap","slug":"soap"},{"emoji":"🫧","skin_tone_support":false,"name":"bubbles","slug":"bubbles"},{"emoji":"🪥","skin_tone_support":false,"name":"toothbrush","slug":"toothbrush"},{"emoji":"🧽","skin_tone_support":false,"name":"sponge","slug":"sponge"},{"emoji":"🧯","skin_tone_support":false,"name":"fire extinguisher","slug":"fire_extinguisher"},{"emoji":"🛒","skin_tone_support":false,"name":"shopping cart","slug":"shopping_cart"},{"emoji":"🚬","skin_tone_support":false,"name":"cigarette","slug":"cigarette"},{"emoji":"⚰️","skin_tone_support":false,"name":"coffin","slug":"coffin"},{"emoji":"🪦","skin_tone_support":false,"name":"headstone","slug":"headstone"},{"emoji":"⚱️","skin_tone_support":false,"name":"funeral urn","slug":"funeral_urn"},{"emoji":"🗿","skin_tone_support":false,"name":"moai","slug":"moai"},{"emoji":"🪧","skin_tone_support":false,"name":"placard","slug":"placard"},{"emoji":"🪪","skin_tone_support":false,"name":"identification card","slug":"identification_card"}],"symbols":[{"emoji":"🏧","skin_tone_support":false,"name":"ATM sign","slug":"atm_sign"},{"emoji":"🚮","skin_tone_support":false,"name":"litter in bin sign","slug":"litter_in_bin_sign"},{"emoji":"🚰","skin_tone_support":false,"name":"potable water","slug":"potable_water"},{"emoji":"♿","skin_tone_support":false,"name":"wheelchair symbol","slug":"wheelchair_symbol"},{"emoji":"🚹","skin_tone_support":false,"name":"men’s room","slug":"men_s_room"},{"emoji":"🚺","skin_tone_support":false,"name":"women’s room","slug":"women_s_room"},{"emoji":"🚻","skin_tone_support":false,"name":"restroom","slug":"restroom"},{"emoji":"🚼","skin_tone_support":false,"name":"baby symbol","slug":"baby_symbol"},{"emoji":"🚾","skin_tone_support":false,"name":"water closet","slug":"water_closet"},{"emoji":"🛂","skin_tone_support":false,"name":"passport control","slug":"passport_control"},{"emoji":"🛃","skin_tone_support":false,"name":"customs","slug":"customs"},{"emoji":"🛄","skin_tone_support":false,"name":"baggage claim","slug":"baggage_claim"},{"emoji":"🛅","skin_tone_support":false,"name":"left luggage","slug":"left_luggage"},{"emoji":"⚠️","skin_tone_support":false,"name":"warning","slug":"warning"},{"emoji":"🚸","skin_tone_support":false,"name":"children crossing","slug":"children_crossing"},{"emoji":"⛔","skin_tone_support":false,"name":"no entry","slug":"no_entry"},{"emoji":"🚫","skin_tone_support":false,"name":"prohibited","slug":"prohibited"},{"emoji":"🚳","skin_tone_support":false,"name":"no bicycles","slug":"no_bicycles"},{"emoji":"🚭","skin_tone_support":false,"name":"no smoking","slug":"no_smoking"},{"emoji":"🚯","skin_tone_support":false,"name":"no littering","slug":"no_littering"},{"emoji":"🚱","skin_tone_support":false,"name":"non-potable water","slug":"non_potable_water"},{"emoji":"🚷","skin_tone_support":false,"name":"no pedestrians","slug":"no_pedestrians"},{"emoji":"📵","skin_tone_support":false,"name":"no mobile phones","slug":"no_mobile_phones"},{"emoji":"🔞","skin_tone_support":false,"name":"no one under eighteen","slug":"no_one_under_eighteen"},{"emoji":"☢️","skin_tone_support":false,"name":"radioactive","slug":"radioactive"},{"emoji":"☣️","skin_tone_support":false,"name":"biohazard","slug":"biohazard"},{"emoji":"⬆️","skin_tone_support":false,"name":"up arrow","slug":"up_arrow"},{"emoji":"↗️","skin_tone_support":false,"name":"up-right arrow","slug":"up_right_arrow"},{"emoji":"➡️","skin_tone_support":false,"name":"right arrow","slug":"right_arrow"},{"emoji":"↘️","skin_tone_support":false,"name":"down-right arrow","slug":"down_right_arrow"},{"emoji":"⬇️","skin_tone_support":false,"name":"down arrow","slug":"down_arrow"},{"emoji":"↙️","skin_tone_support":false,"name":"down-left arrow","slug":"down_left_arrow"},{"emoji":"⬅️","skin_tone_support":false,"name":"left arrow","slug":"left_arrow"},{"emoji":"↖️","skin_tone_support":false,"name":"up-left arrow","slug":"up_left_arrow"},{"emoji":"↕️","skin_tone_support":false,"name":"up-down arrow","slug":"up_down_arrow"},{"emoji":"↔️","skin_tone_support":false,"name":"left-right arrow","slug":"left_right_arrow"},{"emoji":"↩️","skin_tone_support":false,"name":"right arrow curving left","slug":"right_arrow_curving_left"},{"emoji":"↪️","skin_tone_support":false,"name":"left arrow curving right","slug":"left_arrow_curving_right"},{"emoji":"⤴️","skin_tone_support":false,"name":"right arrow curving up","slug":"right_arrow_curving_up"},{"emoji":"⤵️","skin_tone_support":false,"name":"right arrow curving down","slug":"right_arrow_curving_down"},{"emoji":"🔃","skin_tone_support":false,"name":"clockwise vertical arrows","slug":"clockwise_vertical_arrows"},{"emoji":"🔄","skin_tone_support":false,"name":"counterclockwise arrows button","slug":"counterclockwise_arrows_button"},{"emoji":"🔙","skin_tone_support":false,"name":"BACK arrow","slug":"back_arrow"},{"emoji":"🔚","skin_tone_support":false,"name":"END arrow","slug":"end_arrow"},{"emoji":"🔛","skin_tone_support":false,"name":"ON! arrow","slug":"on_arrow"},{"emoji":"🔜","skin_tone_support":false,"name":"SOON arrow","slug":"soon_arrow"},{"emoji":"🔝","skin_tone_support":false,"name":"TOP arrow","slug":"top_arrow"},{"emoji":"🛐","skin_tone_support":false,"name":"place of worship","slug":"place_of_worship"},{"emoji":"⚛️","skin_tone_support":false,"name":"atom symbol","slug":"atom_symbol"},{"emoji":"🕉️","skin_tone_support":false,"name":"om","slug":"om"},{"emoji":"✡️","skin_tone_support":false,"name":"star of David","slug":"star_of_david"},{"emoji":"☸️","skin_tone_support":false,"name":"wheel of dharma","slug":"wheel_of_dharma"},{"emoji":"☯️","skin_tone_support":false,"name":"yin yang","slug":"yin_yang"},{"emoji":"✝️","skin_tone_support":false,"name":"latin cross","slug":"latin_cross"},{"emoji":"☦️","skin_tone_support":false,"name":"orthodox cross","slug":"orthodox_cross"},{"emoji":"☪️","skin_tone_support":false,"name":"star and crescent","slug":"star_and_crescent"},{"emoji":"☮️","skin_tone_support":false,"name":"peace symbol","slug":"peace_symbol"},{"emoji":"🕎","skin_tone_support":false,"name":"menorah","slug":"menorah"},{"emoji":"🔯","skin_tone_support":false,"name":"dotted six-pointed star","slug":"dotted_six_pointed_star"},{"emoji":"♈","skin_tone_support":false,"name":"Aries","slug":"aries"},{"emoji":"♉","skin_tone_support":false,"name":"Taurus","slug":"taurus"},{"emoji":"♊","skin_tone_support":false,"name":"Gemini","slug":"gemini"},{"emoji":"♋","skin_tone_support":false,"name":"Cancer","slug":"cancer"},{"emoji":"♌","skin_tone_support":false,"name":"Leo","slug":"leo"},{"emoji":"♍","skin_tone_support":false,"name":"Virgo","slug":"virgo"},{"emoji":"♎","skin_tone_support":false,"name":"Libra","slug":"libra"},{"emoji":"♏","skin_tone_support":false,"name":"Scorpio","slug":"scorpio"},{"emoji":"♐","skin_tone_support":false,"name":"Sagittarius","slug":"sagittarius"},{"emoji":"♑","skin_tone_support":false,"name":"Capricorn","slug":"capricorn"},{"emoji":"♒","skin_tone_support":false,"name":"Aquarius","slug":"aquarius"},{"emoji":"♓","skin_tone_support":false,"name":"Pisces","slug":"pisces"},{"emoji":"⛎","skin_tone_support":false,"name":"Ophiuchus","slug":"ophiuchus"},{"emoji":"🔀","skin_tone_support":false,"name":"shuffle tracks button","slug":"shuffle_tracks_button"},{"emoji":"🔁","skin_tone_support":false,"name":"repeat button","slug":"repeat_button"},{"emoji":"🔂","skin_tone_support":false,"name":"repeat single button","slug":"repeat_single_button"},{"emoji":"▶️","skin_tone_support":false,"name":"play button","slug":"play_button"},{"emoji":"⏩","skin_tone_support":false,"name":"fast-forward button","slug":"fast_forward_button"},{"emoji":"⏭️","skin_tone_support":false,"name":"next track button","slug":"next_track_button"},{"emoji":"⏯️","skin_tone_support":false,"name":"play or pause button","slug":"play_or_pause_button"},{"emoji":"◀️","skin_tone_support":false,"name":"reverse button","slug":"reverse_button"},{"emoji":"⏪","skin_tone_support":false,"name":"fast reverse button","slug":"fast_reverse_button"},{"emoji":"⏮️","skin_tone_support":false,"name":"last track button","slug":"last_track_button"},{"emoji":"🔼","skin_tone_support":false,"name":"upwards button","slug":"upwards_button"},{"emoji":"⏫","skin_tone_support":false,"name":"fast up button","slug":"fast_up_button"},{"emoji":"🔽","skin_tone_support":false,"name":"downwards button","slug":"downwards_button"},{"emoji":"⏬","skin_tone_support":false,"name":"fast down button","slug":"fast_down_button"},{"emoji":"⏸️","skin_tone_support":false,"name":"pause button","slug":"pause_button"},{"emoji":"⏹️","skin_tone_support":false,"name":"stop button","slug":"stop_button"},{"emoji":"⏺️","skin_tone_support":false,"name":"record button","slug":"record_button"},{"emoji":"⏏️","skin_tone_support":false,"name":"eject button","slug":"eject_button"},{"emoji":"🎦","skin_tone_support":false,"name":"cinema","slug":"cinema"},{"emoji":"🔅","skin_tone_support":false,"name":"dim button","slug":"dim_button"},{"emoji":"🔆","skin_tone_support":false,"name":"bright button","slug":"bright_button"},{"emoji":"📶","skin_tone_support":false,"name":"antenna bars","slug":"antenna_bars"},{"emoji":"📳","skin_tone_support":false,"name":"vibration mode","slug":"vibration_mode"},{"emoji":"📴","skin_tone_support":false,"name":"mobile phone off","slug":"mobile_phone_off"},{"emoji":"♀️","skin_tone_support":false,"name":"female sign","slug":"female_sign"},{"emoji":"♂️","skin_tone_support":false,"name":"male sign","slug":"male_sign"},{"emoji":"⚧️","skin_tone_support":false,"name":"transgender symbol","slug":"transgender_symbol"},{"emoji":"✖️","skin_tone_support":false,"name":"multiply","slug":"multiply"},{"emoji":"➕","skin_tone_support":false,"name":"plus","slug":"plus"},{"emoji":"➖","skin_tone_support":false,"name":"minus","slug":"minus"},{"emoji":"➗","skin_tone_support":false,"name":"divide","slug":"divide"},{"emoji":"🟰","skin_tone_support":false,"name":"heavy equals sign","slug":"heavy_equals_sign"},{"emoji":"♾️","skin_tone_support":false,"name":"infinity","slug":"infinity"},{"emoji":"‼️","skin_tone_support":false,"name":"double exclamation mark","slug":"double_exclamation_mark"},{"emoji":"⁉️","skin_tone_support":false,"name":"exclamation question mark","slug":"exclamation_question_mark"},{"emoji":"❓","skin_tone_support":false,"name":"red question mark","slug":"red_question_mark"},{"emoji":"❔","skin_tone_support":false,"name":"white question mark","slug":"white_question_mark"},{"emoji":"❕","skin_tone_support":false,"name":"white exclamation mark","slug":"white_exclamation_mark"},{"emoji":"❗","skin_tone_support":false,"name":"red exclamation mark","slug":"red_exclamation_mark"},{"emoji":"〰️","skin_tone_support":false,"name":"wavy dash","slug":"wavy_dash"},{"emoji":"💱","skin_tone_support":false,"name":"currency exchange","slug":"currency_exchange"},{"emoji":"💲","skin_tone_support":false,"name":"heavy dollar sign","slug":"heavy_dollar_sign"},{"emoji":"⚕️","skin_tone_support":false,"name":"medical symbol","slug":"medical_symbol"},{"emoji":"♻️","skin_tone_support":false,"name":"recycling symbol","slug":"recycling_symbol"},{"emoji":"⚜️","skin_tone_support":false,"name":"fleur-de-lis","slug":"fleur_de_lis"},{"emoji":"🔱","skin_tone_support":false,"name":"trident emblem","slug":"trident_emblem"},{"emoji":"📛","skin_tone_support":false,"name":"name badge","slug":"name_badge"},{"emoji":"🔰","skin_tone_support":false,"name":"Japanese symbol for beginner","slug":"japanese_symbol_for_beginner"},{"emoji":"⭕","skin_tone_support":false,"name":"hollow red circle","slug":"hollow_red_circle"},{"emoji":"✅","skin_tone_support":false,"name":"check mark button","slug":"check_mark_button"},{"emoji":"☑️","skin_tone_support":false,"name":"check box with check","slug":"check_box_with_check"},{"emoji":"✔️","skin_tone_support":false,"name":"check mark","slug":"check_mark"},{"emoji":"❌","skin_tone_support":false,"name":"cross mark","slug":"cross_mark"},{"emoji":"❎","skin_tone_support":false,"name":"cross mark button","slug":"cross_mark_button"},{"emoji":"➰","skin_tone_support":false,"name":"curly loop","slug":"curly_loop"},{"emoji":"➿","skin_tone_support":false,"name":"double curly loop","slug":"double_curly_loop"},{"emoji":"〽️","skin_tone_support":false,"name":"part alternation mark","slug":"part_alternation_mark"},{"emoji":"✳️","skin_tone_support":false,"name":"eight-spoked asterisk","slug":"eight_spoked_asterisk"},{"emoji":"✴️","skin_tone_support":false,"name":"eight-pointed star","slug":"eight_pointed_star"},{"emoji":"❇️","skin_tone_support":false,"name":"sparkle","slug":"sparkle"},{"emoji":"©️","skin_tone_support":false,"name":"copyright","slug":"copyright"},{"emoji":"®️","skin_tone_support":false,"name":"registered","slug":"registered"},{"emoji":"™️","skin_tone_support":false,"name":"trade mark","slug":"trade_mark"},{"emoji":"#️⃣","skin_tone_support":false,"name":"keycap #","slug":"keycap_"},{"emoji":"*️⃣","skin_tone_support":false,"name":"keycap *","slug":"keycap_"},{"emoji":"0️⃣","skin_tone_support":false,"name":"keycap 0","slug":"keycap_0"},{"emoji":"1️⃣","skin_tone_support":false,"name":"keycap 1","slug":"keycap_1"},{"emoji":"2️⃣","skin_tone_support":false,"name":"keycap 2","slug":"keycap_2"},{"emoji":"3️⃣","skin_tone_support":false,"name":"keycap 3","slug":"keycap_3"},{"emoji":"4️⃣","skin_tone_support":false,"name":"keycap 4","slug":"keycap_4"},{"emoji":"5️⃣","skin_tone_support":false,"name":"keycap 5","slug":"keycap_5"},{"emoji":"6️⃣","skin_tone_support":false,"name":"keycap 6","slug":"keycap_6"},{"emoji":"7️⃣","skin_tone_support":false,"name":"keycap 7","slug":"keycap_7"},{"emoji":"8️⃣","skin_tone_support":false,"name":"keycap 8","slug":"keycap_8"},{"emoji":"9️⃣","skin_tone_support":false,"name":"keycap 9","slug":"keycap_9"},{"emoji":"🔟","skin_tone_support":false,"name":"keycap 10","slug":"keycap_10"},{"emoji":"🔠","skin_tone_support":false,"name":"input latin uppercase","slug":"input_latin_uppercase"},{"emoji":"🔡","skin_tone_support":false,"name":"input latin lowercase","slug":"input_latin_lowercase"},{"emoji":"🔢","skin_tone_support":false,"name":"input numbers","slug":"input_numbers"},{"emoji":"🔣","skin_tone_support":false,"name":"input symbols","slug":"input_symbols"},{"emoji":"🔤","skin_tone_support":false,"name":"input latin letters","slug":"input_latin_letters"},{"emoji":"🅰️","skin_tone_support":false,"name":"A button (blood type)","slug":"a_button"},{"emoji":"🆎","skin_tone_support":false,"name":"AB button (blood type)","slug":"ab_button"},{"emoji":"🅱️","skin_tone_support":false,"name":"B button (blood type)","slug":"b_button"},{"emoji":"🆑","skin_tone_support":false,"name":"CL button","slug":"cl_button"},{"emoji":"🆒","skin_tone_support":false,"name":"COOL button","slug":"cool_button"},{"emoji":"🆓","skin_tone_support":false,"name":"FREE button","slug":"free_button"},{"emoji":"ℹ️","skin_tone_support":false,"name":"information","slug":"information"},{"emoji":"🆔","skin_tone_support":false,"name":"ID button","slug":"id_button"},{"emoji":"Ⓜ️","skin_tone_support":false,"name":"circled M","slug":"circled_m"},{"emoji":"🆕","skin_tone_support":false,"name":"NEW button","slug":"new_button"},{"emoji":"🆖","skin_tone_support":false,"name":"NG button","slug":"ng_button"},{"emoji":"🅾️","skin_tone_support":false,"name":"O button (blood type)","slug":"o_button"},{"emoji":"🆗","skin_tone_support":false,"name":"OK button","slug":"ok_button"},{"emoji":"🅿️","skin_tone_support":false,"name":"P button","slug":"p_button"},{"emoji":"🆘","skin_tone_support":false,"name":"SOS button","slug":"sos_button"},{"emoji":"🆙","skin_tone_support":false,"name":"UP! button","slug":"up_button"},{"emoji":"🆚","skin_tone_support":false,"name":"VS button","slug":"vs_button"},{"emoji":"🈁","skin_tone_support":false,"name":"Japanese “here” button","slug":"japanese_here_button"},{"emoji":"🈂️","skin_tone_support":false,"name":"Japanese “service charge” button","slug":"japanese_service_charge_button"},{"emoji":"🈷️","skin_tone_support":false,"name":"Japanese “monthly amount” button","slug":"japanese_monthly_amount_button"},{"emoji":"🈶","skin_tone_support":false,"name":"Japanese “not free of charge” button","slug":"japanese_not_free_of_charge_button"},{"emoji":"🈯","skin_tone_support":false,"name":"Japanese “reserved” button","slug":"japanese_reserved_button"},{"emoji":"🉐","skin_tone_support":false,"name":"Japanese “bargain” button","slug":"japanese_bargain_button"},{"emoji":"🈹","skin_tone_support":false,"name":"Japanese “discount” button","slug":"japanese_discount_button"},{"emoji":"🈚","skin_tone_support":false,"name":"Japanese “free of charge” button","slug":"japanese_free_of_charge_button"},{"emoji":"🈲","skin_tone_support":false,"name":"Japanese “prohibited” button","slug":"japanese_prohibited_button"},{"emoji":"🉑","skin_tone_support":false,"name":"Japanese “acceptable” button","slug":"japanese_acceptable_button"},{"emoji":"🈸","skin_tone_support":false,"name":"Japanese “application” button","slug":"japanese_application_button"},{"emoji":"🈴","skin_tone_support":false,"name":"Japanese “passing grade” button","slug":"japanese_passing_grade_button"},{"emoji":"🈳","skin_tone_support":false,"name":"Japanese “vacancy” button","slug":"japanese_vacancy_button"},{"emoji":"㊗️","skin_tone_support":false,"name":"Japanese “congratulations” button","slug":"japanese_congratulations_button"},{"emoji":"㊙️","skin_tone_support":false,"name":"Japanese “secret” button","slug":"japanese_secret_button"},{"emoji":"🈺","skin_tone_support":false,"name":"Japanese “open for business” button","slug":"japanese_open_for_business_button"},{"emoji":"🈵","skin_tone_support":false,"name":"Japanese “no vacancy” button","slug":"japanese_no_vacancy_button"},{"emoji":"🔴","skin_tone_support":false,"name":"red circle","slug":"red_circle"},{"emoji":"🟠","skin_tone_support":false,"name":"orange circle","slug":"orange_circle"},{"emoji":"🟡","skin_tone_support":false,"name":"yellow circle","slug":"yellow_circle"},{"emoji":"🟢","skin_tone_support":false,"name":"green circle","slug":"green_circle"},{"emoji":"🔵","skin_tone_support":false,"name":"blue circle","slug":"blue_circle"},{"emoji":"🟣","skin_tone_support":false,"name":"purple circle","slug":"purple_circle"},{"emoji":"🟤","skin_tone_support":false,"name":"brown circle","slug":"brown_circle"},{"emoji":"⚫","skin_tone_support":false,"name":"black circle","slug":"black_circle"},{"emoji":"⚪","skin_tone_support":false,"name":"white circle","slug":"white_circle"},{"emoji":"🟥","skin_tone_support":false,"name":"red square","slug":"red_square"},{"emoji":"🟧","skin_tone_support":false,"name":"orange square","slug":"orange_square"},{"emoji":"🟨","skin_tone_support":false,"name":"yellow square","slug":"yellow_square"},{"emoji":"🟩","skin_tone_support":false,"name":"green square","slug":"green_square"},{"emoji":"🟦","skin_tone_support":false,"name":"blue square","slug":"blue_square"},{"emoji":"🟪","skin_tone_support":false,"name":"purple square","slug":"purple_square"},{"emoji":"🟫","skin_tone_support":false,"name":"brown square","slug":"brown_square"},{"emoji":"⬛","skin_tone_support":false,"name":"black large square","slug":"black_large_square"},{"emoji":"⬜","skin_tone_support":false,"name":"white large square","slug":"white_large_square"},{"emoji":"◼️","skin_tone_support":false,"name":"black medium square","slug":"black_medium_square"},{"emoji":"◻️","skin_tone_support":false,"name":"white medium square","slug":"white_medium_square"},{"emoji":"◾","skin_tone_support":false,"name":"black medium-small square","slug":"black_medium_small_square"},{"emoji":"◽","skin_tone_support":false,"name":"white medium-small square","slug":"white_medium_small_square"},{"emoji":"▪️","skin_tone_support":false,"name":"black small square","slug":"black_small_square"},{"emoji":"▫️","skin_tone_support":false,"name":"white small square","slug":"white_small_square"},{"emoji":"🔶","skin_tone_support":false,"name":"large orange diamond","slug":"large_orange_diamond"},{"emoji":"🔷","skin_tone_support":false,"name":"large blue diamond","slug":"large_blue_diamond"},{"emoji":"🔸","skin_tone_support":false,"name":"small orange diamond","slug":"small_orange_diamond"},{"emoji":"🔹","skin_tone_support":false,"name":"small blue diamond","slug":"small_blue_diamond"},{"emoji":"🔺","skin_tone_support":false,"name":"red triangle pointed up","slug":"red_triangle_pointed_up"},{"emoji":"🔻","skin_tone_support":false,"name":"red triangle pointed down","slug":"red_triangle_pointed_down"},{"emoji":"💠","skin_tone_support":false,"name":"diamond with a dot","slug":"diamond_with_a_dot"},{"emoji":"🔘","skin_tone_support":false,"name":"radio button","slug":"radio_button"},{"emoji":"🔳","skin_tone_support":false,"name":"white square button","slug":"white_square_button"},{"emoji":"🔲","skin_tone_support":false,"name":"black square button","slug":"black_square_button"}],"flags":[{"emoji":"🏁","skin_tone_support":false,"name":"chequered flag","slug":"chequered_flag"},{"emoji":"🚩","skin_tone_support":false,"name":"triangular flag","slug":"triangular_flag"},{"emoji":"🎌","skin_tone_support":false,"name":"crossed flags","slug":"crossed_flags"},{"emoji":"🏴","skin_tone_support":false,"name":"black flag","slug":"black_flag"},{"emoji":"🏳️","skin_tone_support":false,"name":"white flag","slug":"white_flag"},{"emoji":"🏳️🌈","skin_tone_support":false,"name":"rainbow flag","slug":"rainbow_flag"},{"emoji":"🏳️⚧️","skin_tone_support":false,"name":"transgender flag","slug":"transgender_flag"},{"emoji":"🏴☠️","skin_tone_support":false,"name":"pirate flag","slug":"pirate_flag"},{"emoji":"🇦🇨","skin_tone_support":false,"name":"flag Ascension Island","slug":"flag_ascension_island"},{"emoji":"🇦🇩","skin_tone_support":false,"name":"flag Andorra","slug":"flag_andorra"},{"emoji":"🇦🇪","skin_tone_support":false,"name":"flag United Arab Emirates","slug":"flag_united_arab_emirates"},{"emoji":"🇦🇫","skin_tone_support":false,"name":"flag Afghanistan","slug":"flag_afghanistan"},{"emoji":"🇦🇬","skin_tone_support":false,"name":"flag Antigua & Barbuda","slug":"flag_antigua_barbuda"},{"emoji":"🇦🇮","skin_tone_support":false,"name":"flag Anguilla","slug":"flag_anguilla"},{"emoji":"🇦🇱","skin_tone_support":false,"name":"flag Albania","slug":"flag_albania"},{"emoji":"🇦🇲","skin_tone_support":false,"name":"flag Armenia","slug":"flag_armenia"},{"emoji":"🇦🇴","skin_tone_support":false,"name":"flag Angola","slug":"flag_angola"},{"emoji":"🇦🇶","skin_tone_support":false,"name":"flag Antarctica","slug":"flag_antarctica"},{"emoji":"🇦🇷","skin_tone_support":false,"name":"flag Argentina","slug":"flag_argentina"},{"emoji":"🇦🇸","skin_tone_support":false,"name":"flag American Samoa","slug":"flag_american_samoa"},{"emoji":"🇦🇹","skin_tone_support":false,"name":"flag Austria","slug":"flag_austria"},{"emoji":"🇦🇺","skin_tone_support":false,"name":"flag Australia","slug":"flag_australia"},{"emoji":"🇦🇼","skin_tone_support":false,"name":"flag Aruba","slug":"flag_aruba"},{"emoji":"🇦🇽","skin_tone_support":false,"name":"flag Åland Islands","slug":"flag_aland_islands"},{"emoji":"🇦🇿","skin_tone_support":false,"name":"flag Azerbaijan","slug":"flag_azerbaijan"},{"emoji":"🇧🇦","skin_tone_support":false,"name":"flag Bosnia & Herzegovina","slug":"flag_bosnia_herzegovina"},{"emoji":"🇧🇧","skin_tone_support":false,"name":"flag Barbados","slug":"flag_barbados"},{"emoji":"🇧🇩","skin_tone_support":false,"name":"flag Bangladesh","slug":"flag_bangladesh"},{"emoji":"🇧🇪","skin_tone_support":false,"name":"flag Belgium","slug":"flag_belgium"},{"emoji":"🇧🇫","skin_tone_support":false,"name":"flag Burkina Faso","slug":"flag_burkina_faso"},{"emoji":"🇧🇬","skin_tone_support":false,"name":"flag Bulgaria","slug":"flag_bulgaria"},{"emoji":"🇧🇭","skin_tone_support":false,"name":"flag Bahrain","slug":"flag_bahrain"},{"emoji":"🇧🇮","skin_tone_support":false,"name":"flag Burundi","slug":"flag_burundi"},{"emoji":"🇧🇯","skin_tone_support":false,"name":"flag Benin","slug":"flag_benin"},{"emoji":"🇧🇱","skin_tone_support":false,"name":"flag St. Barthélemy","slug":"flag_st_barthelemy"},{"emoji":"🇧🇲","skin_tone_support":false,"name":"flag Bermuda","slug":"flag_bermuda"},{"emoji":"🇧🇳","skin_tone_support":false,"name":"flag Brunei","slug":"flag_brunei"},{"emoji":"🇧🇴","skin_tone_support":false,"name":"flag Bolivia","slug":"flag_bolivia"},{"emoji":"🇧🇶","skin_tone_support":false,"name":"flag Caribbean Netherlands","slug":"flag_caribbean_netherlands"},{"emoji":"🇧🇷","skin_tone_support":false,"name":"flag Brazil","slug":"flag_brazil"},{"emoji":"🇧🇸","skin_tone_support":false,"name":"flag Bahamas","slug":"flag_bahamas"},{"emoji":"🇧🇹","skin_tone_support":false,"name":"flag Bhutan","slug":"flag_bhutan"},{"emoji":"🇧🇻","skin_tone_support":false,"name":"flag Bouvet Island","slug":"flag_bouvet_island"},{"emoji":"🇧🇼","skin_tone_support":false,"name":"flag Botswana","slug":"flag_botswana"},{"emoji":"🇧🇾","skin_tone_support":false,"name":"flag Belarus","slug":"flag_belarus"},{"emoji":"🇧🇿","skin_tone_support":false,"name":"flag Belize","slug":"flag_belize"},{"emoji":"🇨🇦","skin_tone_support":false,"name":"flag Canada","slug":"flag_canada"},{"emoji":"🇨🇨","skin_tone_support":false,"name":"flag Cocos (Keeling) Islands","slug":"flag_cocos_islands"},{"emoji":"🇨🇩","skin_tone_support":false,"name":"flag Congo - Kinshasa","slug":"flag_congo_kinshasa"},{"emoji":"🇨🇫","skin_tone_support":false,"name":"flag Central African Republic","slug":"flag_central_african_republic"},{"emoji":"🇨🇬","skin_tone_support":false,"name":"flag Congo - Brazzaville","slug":"flag_congo_brazzaville"},{"emoji":"🇨🇭","skin_tone_support":false,"name":"flag Switzerland","slug":"flag_switzerland"},{"emoji":"🇨🇮","skin_tone_support":false,"name":"flag Côte d’Ivoire","slug":"flag_cote_d_ivoire"},{"emoji":"🇨🇰","skin_tone_support":false,"name":"flag Cook Islands","slug":"flag_cook_islands"},{"emoji":"🇨🇱","skin_tone_support":false,"name":"flag Chile","slug":"flag_chile"},{"emoji":"🇨🇲","skin_tone_support":false,"name":"flag Cameroon","slug":"flag_cameroon"},{"emoji":"🇨🇳","skin_tone_support":false,"name":"flag China","slug":"flag_china"},{"emoji":"🇨🇴","skin_tone_support":false,"name":"flag Colombia","slug":"flag_colombia"},{"emoji":"🇨🇵","skin_tone_support":false,"name":"flag Clipperton Island","slug":"flag_clipperton_island"},{"emoji":"🇨🇷","skin_tone_support":false,"name":"flag Costa Rica","slug":"flag_costa_rica"},{"emoji":"🇨🇺","skin_tone_support":false,"name":"flag Cuba","slug":"flag_cuba"},{"emoji":"🇨🇻","skin_tone_support":false,"name":"flag Cape Verde","slug":"flag_cape_verde"},{"emoji":"🇨🇼","skin_tone_support":false,"name":"flag Curaçao","slug":"flag_curacao"},{"emoji":"🇨🇽","skin_tone_support":false,"name":"flag Christmas Island","slug":"flag_christmas_island"},{"emoji":"🇨🇾","skin_tone_support":false,"name":"flag Cyprus","slug":"flag_cyprus"},{"emoji":"🇨🇿","skin_tone_support":false,"name":"flag Czechia","slug":"flag_czechia"},{"emoji":"🇩🇪","skin_tone_support":false,"name":"flag Germany","slug":"flag_germany"},{"emoji":"🇩🇬","skin_tone_support":false,"name":"flag Diego Garcia","slug":"flag_diego_garcia"},{"emoji":"🇩🇯","skin_tone_support":false,"name":"flag Djibouti","slug":"flag_djibouti"},{"emoji":"🇩🇰","skin_tone_support":false,"name":"flag Denmark","slug":"flag_denmark"},{"emoji":"🇩🇲","skin_tone_support":false,"name":"flag Dominica","slug":"flag_dominica"},{"emoji":"🇩🇴","skin_tone_support":false,"name":"flag Dominican Republic","slug":"flag_dominican_republic"},{"emoji":"🇩🇿","skin_tone_support":false,"name":"flag Algeria","slug":"flag_algeria"},{"emoji":"🇪🇦","skin_tone_support":false,"name":"flag Ceuta & Melilla","slug":"flag_ceuta_melilla"},{"emoji":"🇪🇨","skin_tone_support":false,"name":"flag Ecuador","slug":"flag_ecuador"},{"emoji":"🇪🇪","skin_tone_support":false,"name":"flag Estonia","slug":"flag_estonia"},{"emoji":"🇪🇬","skin_tone_support":false,"name":"flag Egypt","slug":"flag_egypt"},{"emoji":"🇪🇭","skin_tone_support":false,"name":"flag Western Sahara","slug":"flag_western_sahara"},{"emoji":"🇪🇷","skin_tone_support":false,"name":"flag Eritrea","slug":"flag_eritrea"},{"emoji":"🇪🇸","skin_tone_support":false,"name":"flag Spain","slug":"flag_spain"},{"emoji":"🇪🇹","skin_tone_support":false,"name":"flag Ethiopia","slug":"flag_ethiopia"},{"emoji":"🇪🇺","skin_tone_support":false,"name":"flag European Union","slug":"flag_european_union"},{"emoji":"🇫🇮","skin_tone_support":false,"name":"flag Finland","slug":"flag_finland"},{"emoji":"🇫🇯","skin_tone_support":false,"name":"flag Fiji","slug":"flag_fiji"},{"emoji":"🇫🇰","skin_tone_support":false,"name":"flag Falkland Islands","slug":"flag_falkland_islands"},{"emoji":"🇫🇲","skin_tone_support":false,"name":"flag Micronesia","slug":"flag_micronesia"},{"emoji":"🇫🇴","skin_tone_support":false,"name":"flag Faroe Islands","slug":"flag_faroe_islands"},{"emoji":"🇫🇷","skin_tone_support":false,"name":"flag France","slug":"flag_france"},{"emoji":"🇬🇦","skin_tone_support":false,"name":"flag Gabon","slug":"flag_gabon"},{"emoji":"🇬🇧","skin_tone_support":false,"name":"flag United Kingdom","slug":"flag_united_kingdom"},{"emoji":"🇬🇩","skin_tone_support":false,"name":"flag Grenada","slug":"flag_grenada"},{"emoji":"🇬🇪","skin_tone_support":false,"name":"flag Georgia","slug":"flag_georgia"},{"emoji":"🇬🇫","skin_tone_support":false,"name":"flag French Guiana","slug":"flag_french_guiana"},{"emoji":"🇬🇬","skin_tone_support":false,"name":"flag Guernsey","slug":"flag_guernsey"},{"emoji":"🇬🇭","skin_tone_support":false,"name":"flag Ghana","slug":"flag_ghana"},{"emoji":"🇬🇮","skin_tone_support":false,"name":"flag Gibraltar","slug":"flag_gibraltar"},{"emoji":"🇬🇱","skin_tone_support":false,"name":"flag Greenland","slug":"flag_greenland"},{"emoji":"🇬🇲","skin_tone_support":false,"name":"flag Gambia","slug":"flag_gambia"},{"emoji":"🇬🇳","skin_tone_support":false,"name":"flag Guinea","slug":"flag_guinea"},{"emoji":"🇬🇵","skin_tone_support":false,"name":"flag Guadeloupe","slug":"flag_guadeloupe"},{"emoji":"🇬🇶","skin_tone_support":false,"name":"flag Equatorial Guinea","slug":"flag_equatorial_guinea"},{"emoji":"🇬🇷","skin_tone_support":false,"name":"flag Greece","slug":"flag_greece"},{"emoji":"🇬🇸","skin_tone_support":false,"name":"flag South Georgia & South Sandwich Islands","slug":"flag_south_georgia_south_sandwich_islands"},{"emoji":"🇬🇹","skin_tone_support":false,"name":"flag Guatemala","slug":"flag_guatemala"},{"emoji":"🇬🇺","skin_tone_support":false,"name":"flag Guam","slug":"flag_guam"},{"emoji":"🇬🇼","skin_tone_support":false,"name":"flag Guinea-Bissau","slug":"flag_guinea_bissau"},{"emoji":"🇬🇾","skin_tone_support":false,"name":"flag Guyana","slug":"flag_guyana"},{"emoji":"🇭🇰","skin_tone_support":false,"name":"flag Hong Kong SAR China","slug":"flag_hong_kong_sar_china"},{"emoji":"🇭🇲","skin_tone_support":false,"name":"flag Heard & McDonald Islands","slug":"flag_heard_mcdonald_islands"},{"emoji":"🇭🇳","skin_tone_support":false,"name":"flag Honduras","slug":"flag_honduras"},{"emoji":"🇭🇷","skin_tone_support":false,"name":"flag Croatia","slug":"flag_croatia"},{"emoji":"🇭🇹","skin_tone_support":false,"name":"flag Haiti","slug":"flag_haiti"},{"emoji":"🇭🇺","skin_tone_support":false,"name":"flag Hungary","slug":"flag_hungary"},{"emoji":"🇮🇨","skin_tone_support":false,"name":"flag Canary Islands","slug":"flag_canary_islands"},{"emoji":"🇮🇩","skin_tone_support":false,"name":"flag Indonesia","slug":"flag_indonesia"},{"emoji":"🇮🇪","skin_tone_support":false,"name":"flag Ireland","slug":"flag_ireland"},{"emoji":"🇮🇱","skin_tone_support":false,"name":"flag Israel","slug":"flag_israel"},{"emoji":"🇮🇲","skin_tone_support":false,"name":"flag Isle of Man","slug":"flag_isle_of_man"},{"emoji":"🇮🇳","skin_tone_support":false,"name":"flag India","slug":"flag_india"},{"emoji":"🇮🇴","skin_tone_support":false,"name":"flag British Indian Ocean Territory","slug":"flag_british_indian_ocean_territory"},{"emoji":"🇮🇶","skin_tone_support":false,"name":"flag Iraq","slug":"flag_iraq"},{"emoji":"🇮🇷","skin_tone_support":false,"name":"flag Iran","slug":"flag_iran"},{"emoji":"🇮🇸","skin_tone_support":false,"name":"flag Iceland","slug":"flag_iceland"},{"emoji":"🇮🇹","skin_tone_support":false,"name":"flag Italy","slug":"flag_italy"},{"emoji":"🇯🇪","skin_tone_support":false,"name":"flag Jersey","slug":"flag_jersey"},{"emoji":"🇯🇲","skin_tone_support":false,"name":"flag Jamaica","slug":"flag_jamaica"},{"emoji":"🇯🇴","skin_tone_support":false,"name":"flag Jordan","slug":"flag_jordan"},{"emoji":"🇯🇵","skin_tone_support":false,"name":"flag Japan","slug":"flag_japan"},{"emoji":"🇰🇪","skin_tone_support":false,"name":"flag Kenya","slug":"flag_kenya"},{"emoji":"🇰🇬","skin_tone_support":false,"name":"flag Kyrgyzstan","slug":"flag_kyrgyzstan"},{"emoji":"🇰🇭","skin_tone_support":false,"name":"flag Cambodia","slug":"flag_cambodia"},{"emoji":"🇰🇮","skin_tone_support":false,"name":"flag Kiribati","slug":"flag_kiribati"},{"emoji":"🇰🇲","skin_tone_support":false,"name":"flag Comoros","slug":"flag_comoros"},{"emoji":"🇰🇳","skin_tone_support":false,"name":"flag St. Kitts & Nevis","slug":"flag_st_kitts_nevis"},{"emoji":"🇰🇵","skin_tone_support":false,"name":"flag North Korea","slug":"flag_north_korea"},{"emoji":"🇰🇷","skin_tone_support":false,"name":"flag South Korea","slug":"flag_south_korea"},{"emoji":"🇰🇼","skin_tone_support":false,"name":"flag Kuwait","slug":"flag_kuwait"},{"emoji":"🇰🇾","skin_tone_support":false,"name":"flag Cayman Islands","slug":"flag_cayman_islands"},{"emoji":"🇰🇿","skin_tone_support":false,"name":"flag Kazakhstan","slug":"flag_kazakhstan"},{"emoji":"🇱🇦","skin_tone_support":false,"name":"flag Laos","slug":"flag_laos"},{"emoji":"🇱🇧","skin_tone_support":false,"name":"flag Lebanon","slug":"flag_lebanon"},{"emoji":"🇱🇨","skin_tone_support":false,"name":"flag St. Lucia","slug":"flag_st_lucia"},{"emoji":"🇱🇮","skin_tone_support":false,"name":"flag Liechtenstein","slug":"flag_liechtenstein"},{"emoji":"🇱🇰","skin_tone_support":false,"name":"flag Sri Lanka","slug":"flag_sri_lanka"},{"emoji":"🇱🇷","skin_tone_support":false,"name":"flag Liberia","slug":"flag_liberia"},{"emoji":"🇱🇸","skin_tone_support":false,"name":"flag Lesotho","slug":"flag_lesotho"},{"emoji":"🇱🇹","skin_tone_support":false,"name":"flag Lithuania","slug":"flag_lithuania"},{"emoji":"🇱🇺","skin_tone_support":false,"name":"flag Luxembourg","slug":"flag_luxembourg"},{"emoji":"🇱🇻","skin_tone_support":false,"name":"flag Latvia","slug":"flag_latvia"},{"emoji":"🇱🇾","skin_tone_support":false,"name":"flag Libya","slug":"flag_libya"},{"emoji":"🇲🇦","skin_tone_support":false,"name":"flag Morocco","slug":"flag_morocco"},{"emoji":"🇲🇨","skin_tone_support":false,"name":"flag Monaco","slug":"flag_monaco"},{"emoji":"🇲🇩","skin_tone_support":false,"name":"flag Moldova","slug":"flag_moldova"},{"emoji":"🇲🇪","skin_tone_support":false,"name":"flag Montenegro","slug":"flag_montenegro"},{"emoji":"🇲🇫","skin_tone_support":false,"name":"flag St. Martin","slug":"flag_st_martin"},{"emoji":"🇲🇬","skin_tone_support":false,"name":"flag Madagascar","slug":"flag_madagascar"},{"emoji":"🇲🇭","skin_tone_support":false,"name":"flag Marshall Islands","slug":"flag_marshall_islands"},{"emoji":"🇲🇰","skin_tone_support":false,"name":"flag North Macedonia","slug":"flag_north_macedonia"},{"emoji":"🇲🇱","skin_tone_support":false,"name":"flag Mali","slug":"flag_mali"},{"emoji":"🇲🇲","skin_tone_support":false,"name":"flag Myanmar (Burma)","slug":"flag_myanmar"},{"emoji":"🇲🇳","skin_tone_support":false,"name":"flag Mongolia","slug":"flag_mongolia"},{"emoji":"🇲🇴","skin_tone_support":false,"name":"flag Macao SAR China","slug":"flag_macao_sar_china"},{"emoji":"🇲🇵","skin_tone_support":false,"name":"flag Northern Mariana Islands","slug":"flag_northern_mariana_islands"},{"emoji":"🇲🇶","skin_tone_support":false,"name":"flag Martinique","slug":"flag_martinique"},{"emoji":"🇲🇷","skin_tone_support":false,"name":"flag Mauritania","slug":"flag_mauritania"},{"emoji":"🇲🇸","skin_tone_support":false,"name":"flag Montserrat","slug":"flag_montserrat"},{"emoji":"🇲🇹","skin_tone_support":false,"name":"flag Malta","slug":"flag_malta"},{"emoji":"🇲🇺","skin_tone_support":false,"name":"flag Mauritius","slug":"flag_mauritius"},{"emoji":"🇲🇻","skin_tone_support":false,"name":"flag Maldives","slug":"flag_maldives"},{"emoji":"🇲🇼","skin_tone_support":false,"name":"flag Malawi","slug":"flag_malawi"},{"emoji":"🇲🇽","skin_tone_support":false,"name":"flag Mexico","slug":"flag_mexico"},{"emoji":"🇲🇾","skin_tone_support":false,"name":"flag Malaysia","slug":"flag_malaysia"},{"emoji":"🇲🇿","skin_tone_support":false,"name":"flag Mozambique","slug":"flag_mozambique"},{"emoji":"🇳🇦","skin_tone_support":false,"name":"flag Namibia","slug":"flag_namibia"},{"emoji":"🇳🇨","skin_tone_support":false,"name":"flag New Caledonia","slug":"flag_new_caledonia"},{"emoji":"🇳🇪","skin_tone_support":false,"name":"flag Niger","slug":"flag_niger"},{"emoji":"🇳🇫","skin_tone_support":false,"name":"flag Norfolk Island","slug":"flag_norfolk_island"},{"emoji":"🇳🇬","skin_tone_support":false,"name":"flag Nigeria","slug":"flag_nigeria"},{"emoji":"🇳🇮","skin_tone_support":false,"name":"flag Nicaragua","slug":"flag_nicaragua"},{"emoji":"🇳🇱","skin_tone_support":false,"name":"flag Netherlands","slug":"flag_netherlands"},{"emoji":"🇳🇴","skin_tone_support":false,"name":"flag Norway","slug":"flag_norway"},{"emoji":"🇳🇵","skin_tone_support":false,"name":"flag Nepal","slug":"flag_nepal"},{"emoji":"🇳🇷","skin_tone_support":false,"name":"flag Nauru","slug":"flag_nauru"},{"emoji":"🇳🇺","skin_tone_support":false,"name":"flag Niue","slug":"flag_niue"},{"emoji":"🇳🇿","skin_tone_support":false,"name":"flag New Zealand","slug":"flag_new_zealand"},{"emoji":"🇴🇲","skin_tone_support":false,"name":"flag Oman","slug":"flag_oman"},{"emoji":"🇵🇦","skin_tone_support":false,"name":"flag Panama","slug":"flag_panama"},{"emoji":"🇵🇪","skin_tone_support":false,"name":"flag Peru","slug":"flag_peru"},{"emoji":"🇵🇫","skin_tone_support":false,"name":"flag French Polynesia","slug":"flag_french_polynesia"},{"emoji":"🇵🇬","skin_tone_support":false,"name":"flag Papua New Guinea","slug":"flag_papua_new_guinea"},{"emoji":"🇵🇭","skin_tone_support":false,"name":"flag Philippines","slug":"flag_philippines"},{"emoji":"🇵🇰","skin_tone_support":false,"name":"flag Pakistan","slug":"flag_pakistan"},{"emoji":"🇵🇱","skin_tone_support":false,"name":"flag Poland","slug":"flag_poland"},{"emoji":"🇵🇲","skin_tone_support":false,"name":"flag St. Pierre & Miquelon","slug":"flag_st_pierre_miquelon"},{"emoji":"🇵🇳","skin_tone_support":false,"name":"flag Pitcairn Islands","slug":"flag_pitcairn_islands"},{"emoji":"🇵🇷","skin_tone_support":false,"name":"flag Puerto Rico","slug":"flag_puerto_rico"},{"emoji":"🇵🇸","skin_tone_support":false,"name":"flag Palestinian Territories","slug":"flag_palestinian_territories"},{"emoji":"🇵🇹","skin_tone_support":false,"name":"flag Portugal","slug":"flag_portugal"},{"emoji":"🇵🇼","skin_tone_support":false,"name":"flag Palau","slug":"flag_palau"},{"emoji":"🇵🇾","skin_tone_support":false,"name":"flag Paraguay","slug":"flag_paraguay"},{"emoji":"🇶🇦","skin_tone_support":false,"name":"flag Qatar","slug":"flag_qatar"},{"emoji":"🇷🇪","skin_tone_support":false,"name":"flag Réunion","slug":"flag_reunion"},{"emoji":"🇷🇴","skin_tone_support":false,"name":"flag Romania","slug":"flag_romania"},{"emoji":"🇷🇸","skin_tone_support":false,"name":"flag Serbia","slug":"flag_serbia"},{"emoji":"🇷🇺","skin_tone_support":false,"name":"flag Russia","slug":"flag_russia"},{"emoji":"🇷🇼","skin_tone_support":false,"name":"flag Rwanda","slug":"flag_rwanda"},{"emoji":"🇸🇦","skin_tone_support":false,"name":"flag Saudi Arabia","slug":"flag_saudi_arabia"},{"emoji":"🇸🇧","skin_tone_support":false,"name":"flag Solomon Islands","slug":"flag_solomon_islands"},{"emoji":"🇸🇨","skin_tone_support":false,"name":"flag Seychelles","slug":"flag_seychelles"},{"emoji":"🇸🇩","skin_tone_support":false,"name":"flag Sudan","slug":"flag_sudan"},{"emoji":"🇸🇪","skin_tone_support":false,"name":"flag Sweden","slug":"flag_sweden"},{"emoji":"🇸🇬","skin_tone_support":false,"name":"flag Singapore","slug":"flag_singapore"},{"emoji":"🇸🇭","skin_tone_support":false,"name":"flag St. Helena","slug":"flag_st_helena"},{"emoji":"🇸🇮","skin_tone_support":false,"name":"flag Slovenia","slug":"flag_slovenia"},{"emoji":"🇸🇯","skin_tone_support":false,"name":"flag Svalbard & Jan Mayen","slug":"flag_svalbard_jan_mayen"},{"emoji":"🇸🇰","skin_tone_support":false,"name":"flag Slovakia","slug":"flag_slovakia"},{"emoji":"🇸🇱","skin_tone_support":false,"name":"flag Sierra Leone","slug":"flag_sierra_leone"},{"emoji":"🇸🇲","skin_tone_support":false,"name":"flag San Marino","slug":"flag_san_marino"},{"emoji":"🇸🇳","skin_tone_support":false,"name":"flag Senegal","slug":"flag_senegal"},{"emoji":"🇸🇴","skin_tone_support":false,"name":"flag Somalia","slug":"flag_somalia"},{"emoji":"🇸🇷","skin_tone_support":false,"name":"flag Suriname","slug":"flag_suriname"},{"emoji":"🇸🇸","skin_tone_support":false,"name":"flag South Sudan","slug":"flag_south_sudan"},{"emoji":"🇸🇹","skin_tone_support":false,"name":"flag São Tomé & Príncipe","slug":"flag_sao_tome_principe"},{"emoji":"🇸🇻","skin_tone_support":false,"name":"flag El Salvador","slug":"flag_el_salvador"},{"emoji":"🇸🇽","skin_tone_support":false,"name":"flag Sint Maarten","slug":"flag_sint_maarten"},{"emoji":"🇸🇾","skin_tone_support":false,"name":"flag Syria","slug":"flag_syria"},{"emoji":"🇸🇿","skin_tone_support":false,"name":"flag Eswatini","slug":"flag_eswatini"},{"emoji":"🇹🇦","skin_tone_support":false,"name":"flag Tristan da Cunha","slug":"flag_tristan_da_cunha"},{"emoji":"🇹🇨","skin_tone_support":false,"name":"flag Turks & Caicos Islands","slug":"flag_turks_caicos_islands"},{"emoji":"🇹🇩","skin_tone_support":false,"name":"flag Chad","slug":"flag_chad"},{"emoji":"🇹🇫","skin_tone_support":false,"name":"flag French Southern Territories","slug":"flag_french_southern_territories"},{"emoji":"🇹🇬","skin_tone_support":false,"name":"flag Togo","slug":"flag_togo"},{"emoji":"🇹🇭","skin_tone_support":false,"name":"flag Thailand","slug":"flag_thailand"},{"emoji":"🇹🇯","skin_tone_support":false,"name":"flag Tajikistan","slug":"flag_tajikistan"},{"emoji":"🇹🇰","skin_tone_support":false,"name":"flag Tokelau","slug":"flag_tokelau"},{"emoji":"🇹🇱","skin_tone_support":false,"name":"flag Timor-Leste","slug":"flag_timor_leste"},{"emoji":"🇹🇲","skin_tone_support":false,"name":"flag Turkmenistan","slug":"flag_turkmenistan"},{"emoji":"🇹🇳","skin_tone_support":false,"name":"flag Tunisia","slug":"flag_tunisia"},{"emoji":"🇹🇴","skin_tone_support":false,"name":"flag Tonga","slug":"flag_tonga"},{"emoji":"🇹🇷","skin_tone_support":false,"name":"flag Turkey","slug":"flag_turkey"},{"emoji":"🇹🇹","skin_tone_support":false,"name":"flag Trinidad & Tobago","slug":"flag_trinidad_tobago"},{"emoji":"🇹🇻","skin_tone_support":false,"name":"flag Tuvalu","slug":"flag_tuvalu"},{"emoji":"🇹🇼","skin_tone_support":false,"name":"flag Taiwan","slug":"flag_taiwan"},{"emoji":"🇹🇿","skin_tone_support":false,"name":"flag Tanzania","slug":"flag_tanzania"},{"emoji":"🇺🇦","skin_tone_support":false,"name":"flag Ukraine","slug":"flag_ukraine"},{"emoji":"🇺🇬","skin_tone_support":false,"name":"flag Uganda","slug":"flag_uganda"},{"emoji":"🇺🇲","skin_tone_support":false,"name":"flag U.S. Outlying Islands","slug":"flag_u_s_outlying_islands"},{"emoji":"🇺🇳","skin_tone_support":false,"name":"flag United Nations","slug":"flag_united_nations"},{"emoji":"🇺🇸","skin_tone_support":false,"name":"flag United States","slug":"flag_united_states"},{"emoji":"🇺🇾","skin_tone_support":false,"name":"flag Uruguay","slug":"flag_uruguay"},{"emoji":"🇺🇿","skin_tone_support":false,"name":"flag Uzbekistan","slug":"flag_uzbekistan"},{"emoji":"🇻🇦","skin_tone_support":false,"name":"flag Vatican City","slug":"flag_vatican_city"},{"emoji":"🇻🇨","skin_tone_support":false,"name":"flag St. Vincent & Grenadines","slug":"flag_st_vincent_grenadines"},{"emoji":"🇻🇪","skin_tone_support":false,"name":"flag Venezuela","slug":"flag_venezuela"},{"emoji":"🇻🇬","skin_tone_support":false,"name":"flag British Virgin Islands","slug":"flag_british_virgin_islands"},{"emoji":"🇻🇮","skin_tone_support":false,"name":"flag U.S. Virgin Islands","slug":"flag_u_s_virgin_islands"},{"emoji":"🇻🇳","skin_tone_support":false,"name":"flag Vietnam","slug":"flag_vietnam"},{"emoji":"🇻🇺","skin_tone_support":false,"name":"flag Vanuatu","slug":"flag_vanuatu"},{"emoji":"🇼🇫","skin_tone_support":false,"name":"flag Wallis & Futuna","slug":"flag_wallis_futuna"},{"emoji":"🇼🇸","skin_tone_support":false,"name":"flag Samoa","slug":"flag_samoa"},{"emoji":"🇽🇰","skin_tone_support":false,"name":"flag Kosovo","slug":"flag_kosovo"},{"emoji":"🇾🇪","skin_tone_support":false,"name":"flag Yemen","slug":"flag_yemen"},{"emoji":"🇾🇹","skin_tone_support":false,"name":"flag Mayotte","slug":"flag_mayotte"},{"emoji":"🇿🇦","skin_tone_support":false,"name":"flag South Africa","slug":"flag_south_africa"},{"emoji":"🇿🇲","skin_tone_support":false,"name":"flag Zambia","slug":"flag_zambia"},{"emoji":"🇿🇼","skin_tone_support":false,"name":"flag Zimbabwe","slug":"flag_zimbabwe"},{"emoji":"🏴","skin_tone_support":false,"name":"flag England","slug":"flag_england"},{"emoji":"🏴","skin_tone_support":false,"name":"flag Scotland","slug":"flag_scotland"},{"emoji":"🏴","skin_tone_support":false,"name":"flag Wales","slug":"flag_wales"}]}
\ No newline at end of file
diff --git a/priv/static/static/img/nsfw.74818f9.png b/priv/static/static/img/nsfw.2958239.png
similarity index 100%
rename from priv/static/static/img/nsfw.74818f9.png
rename to priv/static/static/img/nsfw.2958239.png
diff --git a/priv/static/static/img/pleromatan_apology.f9d5180.png b/priv/static/static/img/pleromatan_apology.f9d5180.png
new file mode 100644
index 000000000..36ad7aeb8
Binary files /dev/null and b/priv/static/static/img/pleromatan_apology.f9d5180.png differ
diff --git a/priv/static/static/img/pleromatan_apology_fox.038b0bb.png b/priv/static/static/img/pleromatan_apology_fox.038b0bb.png
new file mode 100644
index 000000000..17f87694c
Binary files /dev/null and b/priv/static/static/img/pleromatan_apology_fox.038b0bb.png differ
diff --git a/priv/static/static/js/10.02ffbc25214f297f720f.js b/priv/static/static/js/10.02ffbc25214f297f720f.js
deleted file mode 100644
index fbe426710..000000000
Binary files a/priv/static/static/js/10.02ffbc25214f297f720f.js and /dev/null differ
diff --git a/priv/static/static/js/10.02ffbc25214f297f720f.js.map b/priv/static/static/js/10.02ffbc25214f297f720f.js.map
deleted file mode 100644
index 6b230613d..000000000
Binary files a/priv/static/static/js/10.02ffbc25214f297f720f.js.map and /dev/null differ
diff --git a/priv/static/static/js/11.c173c6036fb3af5581b3.js b/priv/static/static/js/11.c173c6036fb3af5581b3.js
deleted file mode 100644
index b693d4c53..000000000
Binary files a/priv/static/static/js/11.c173c6036fb3af5581b3.js and /dev/null differ
diff --git a/priv/static/static/js/11.c173c6036fb3af5581b3.js.map b/priv/static/static/js/11.c173c6036fb3af5581b3.js.map
deleted file mode 100644
index 6fc07fd8a..000000000
Binary files a/priv/static/static/js/11.c173c6036fb3af5581b3.js.map and /dev/null differ
diff --git a/priv/static/static/js/12.5ca41e245bb40263bc7f.js b/priv/static/static/js/12.5ca41e245bb40263bc7f.js
deleted file mode 100644
index a22fcc522..000000000
Binary files a/priv/static/static/js/12.5ca41e245bb40263bc7f.js and /dev/null differ
diff --git a/priv/static/static/js/12.5ca41e245bb40263bc7f.js.map b/priv/static/static/js/12.5ca41e245bb40263bc7f.js.map
deleted file mode 100644
index 762172484..000000000
Binary files a/priv/static/static/js/12.5ca41e245bb40263bc7f.js.map and /dev/null differ
diff --git a/priv/static/static/js/13.99621e6c47936075b44d.js b/priv/static/static/js/13.99621e6c47936075b44d.js
deleted file mode 100644
index ef26b927b..000000000
Binary files a/priv/static/static/js/13.99621e6c47936075b44d.js and /dev/null differ
diff --git a/priv/static/static/js/13.99621e6c47936075b44d.js.map b/priv/static/static/js/13.99621e6c47936075b44d.js.map
deleted file mode 100644
index eb79bff03..000000000
Binary files a/priv/static/static/js/13.99621e6c47936075b44d.js.map and /dev/null differ
diff --git a/priv/static/static/js/14.4e05e7c284119777ecc5.js b/priv/static/static/js/14.4e05e7c284119777ecc5.js
deleted file mode 100644
index 6f5728bf6..000000000
Binary files a/priv/static/static/js/14.4e05e7c284119777ecc5.js and /dev/null differ
diff --git a/priv/static/static/js/14.4e05e7c284119777ecc5.js.map b/priv/static/static/js/14.4e05e7c284119777ecc5.js.map
deleted file mode 100644
index d219c6115..000000000
Binary files a/priv/static/static/js/14.4e05e7c284119777ecc5.js.map and /dev/null differ
diff --git a/priv/static/static/js/15.23f179cc3adc903bb537.js b/priv/static/static/js/15.23f179cc3adc903bb537.js
deleted file mode 100644
index d87608e34..000000000
Binary files a/priv/static/static/js/15.23f179cc3adc903bb537.js and /dev/null differ
diff --git a/priv/static/static/js/15.23f179cc3adc903bb537.js.map b/priv/static/static/js/15.23f179cc3adc903bb537.js.map
deleted file mode 100644
index 15811ea18..000000000
Binary files a/priv/static/static/js/15.23f179cc3adc903bb537.js.map and /dev/null differ
diff --git a/priv/static/static/js/159.903e90c9de8ef6c67077.js b/priv/static/static/js/159.903e90c9de8ef6c67077.js
new file mode 100644
index 000000000..c910bd987
Binary files /dev/null and b/priv/static/static/js/159.903e90c9de8ef6c67077.js differ
diff --git a/priv/static/static/js/159.903e90c9de8ef6c67077.js.map b/priv/static/static/js/159.903e90c9de8ef6c67077.js.map
new file mode 100644
index 000000000..73a1c555c
Binary files /dev/null and b/priv/static/static/js/159.903e90c9de8ef6c67077.js.map differ
diff --git a/priv/static/static/js/16.43dd2c64dcb160dd96a6.js b/priv/static/static/js/16.43dd2c64dcb160dd96a6.js
deleted file mode 100644
index abed0132f..000000000
Binary files a/priv/static/static/js/16.43dd2c64dcb160dd96a6.js and /dev/null differ
diff --git a/priv/static/static/js/16.43dd2c64dcb160dd96a6.js.map b/priv/static/static/js/16.43dd2c64dcb160dd96a6.js.map
deleted file mode 100644
index 20ab38e81..000000000
Binary files a/priv/static/static/js/16.43dd2c64dcb160dd96a6.js.map and /dev/null differ
diff --git a/priv/static/static/js/17.d1deeeb81b7cab98b068.js b/priv/static/static/js/17.d1deeeb81b7cab98b068.js
deleted file mode 100644
index 519a6e2bd..000000000
Binary files a/priv/static/static/js/17.d1deeeb81b7cab98b068.js and /dev/null differ
diff --git a/priv/static/static/js/17.d1deeeb81b7cab98b068.js.map b/priv/static/static/js/17.d1deeeb81b7cab98b068.js.map
deleted file mode 100644
index 156fad930..000000000
Binary files a/priv/static/static/js/17.d1deeeb81b7cab98b068.js.map and /dev/null differ
diff --git a/priv/static/static/js/18.a4d5b399e228a6a45a7b.js b/priv/static/static/js/18.a4d5b399e228a6a45a7b.js
deleted file mode 100644
index 1b17be977..000000000
Binary files a/priv/static/static/js/18.a4d5b399e228a6a45a7b.js and /dev/null differ
diff --git a/priv/static/static/js/18.a4d5b399e228a6a45a7b.js.map b/priv/static/static/js/18.a4d5b399e228a6a45a7b.js.map
deleted file mode 100644
index 5e5264405..000000000
Binary files a/priv/static/static/js/18.a4d5b399e228a6a45a7b.js.map and /dev/null differ
diff --git a/priv/static/static/js/19.e513835c3274271258fa.js b/priv/static/static/js/19.e513835c3274271258fa.js
deleted file mode 100644
index 1a4c2d230..000000000
Binary files a/priv/static/static/js/19.e513835c3274271258fa.js and /dev/null differ
diff --git a/priv/static/static/js/19.e513835c3274271258fa.js.map b/priv/static/static/js/19.e513835c3274271258fa.js.map
deleted file mode 100644
index d92c8eeac..000000000
Binary files a/priv/static/static/js/19.e513835c3274271258fa.js.map and /dev/null differ
diff --git a/priv/static/static/js/2.fec2056b00b4fa3921ba.js b/priv/static/static/js/2.fec2056b00b4fa3921ba.js
deleted file mode 100644
index 483720e2f..000000000
Binary files a/priv/static/static/js/2.fec2056b00b4fa3921ba.js and /dev/null differ
diff --git a/priv/static/static/js/2.fec2056b00b4fa3921ba.js.map b/priv/static/static/js/2.fec2056b00b4fa3921ba.js.map
deleted file mode 100644
index 31d328177..000000000
Binary files a/priv/static/static/js/2.fec2056b00b4fa3921ba.js.map and /dev/null differ
diff --git a/priv/static/static/js/20.683b112f4dcea887f707.js b/priv/static/static/js/20.683b112f4dcea887f707.js
deleted file mode 100644
index 726530149..000000000
Binary files a/priv/static/static/js/20.683b112f4dcea887f707.js and /dev/null differ
diff --git a/priv/static/static/js/20.683b112f4dcea887f707.js.map b/priv/static/static/js/20.683b112f4dcea887f707.js.map
deleted file mode 100644
index 094f913db..000000000
Binary files a/priv/static/static/js/20.683b112f4dcea887f707.js.map and /dev/null differ
diff --git a/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js b/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js
deleted file mode 100644
index c363a2197..000000000
Binary files a/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js and /dev/null differ
diff --git a/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js.map b/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js.map
deleted file mode 100644
index b5b25eb31..000000000
Binary files a/priv/static/static/js/21.b2844ccdcfc3c8191e8e.js.map and /dev/null differ
diff --git a/priv/static/static/js/22.68c0a771d79e3383f5e8.js b/priv/static/static/js/22.68c0a771d79e3383f5e8.js
deleted file mode 100644
index f982b241b..000000000
Binary files a/priv/static/static/js/22.68c0a771d79e3383f5e8.js and /dev/null differ
diff --git a/priv/static/static/js/22.68c0a771d79e3383f5e8.js.map b/priv/static/static/js/22.68c0a771d79e3383f5e8.js.map
deleted file mode 100644
index 10a44dd2e..000000000
Binary files a/priv/static/static/js/22.68c0a771d79e3383f5e8.js.map and /dev/null differ
diff --git a/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js b/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js
deleted file mode 100644
index 3d6701989..000000000
Binary files a/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js and /dev/null differ
diff --git a/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js.map b/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js.map
deleted file mode 100644
index f5200b9dc..000000000
Binary files a/priv/static/static/js/23.0b6cdf4c9dc52c4291c0.js.map and /dev/null differ
diff --git a/priv/static/static/js/24.5cfb87799bd882b933dd.js b/priv/static/static/js/24.5cfb87799bd882b933dd.js
deleted file mode 100644
index 811c4fa52..000000000
Binary files a/priv/static/static/js/24.5cfb87799bd882b933dd.js and /dev/null differ
diff --git a/priv/static/static/js/24.5cfb87799bd882b933dd.js.map b/priv/static/static/js/24.5cfb87799bd882b933dd.js.map
deleted file mode 100644
index c03306f8a..000000000
Binary files a/priv/static/static/js/24.5cfb87799bd882b933dd.js.map and /dev/null differ
diff --git a/priv/static/static/js/25.8185e4d775cea9fe47e1.js b/priv/static/static/js/25.8185e4d775cea9fe47e1.js
deleted file mode 100644
index ca0e22957..000000000
Binary files a/priv/static/static/js/25.8185e4d775cea9fe47e1.js and /dev/null differ
diff --git a/priv/static/static/js/25.8185e4d775cea9fe47e1.js.map b/priv/static/static/js/25.8185e4d775cea9fe47e1.js.map
deleted file mode 100644
index d559ea56b..000000000
Binary files a/priv/static/static/js/25.8185e4d775cea9fe47e1.js.map and /dev/null differ
diff --git a/priv/static/static/js/26.34ec129dd8f860ce4a8e.js b/priv/static/static/js/26.34ec129dd8f860ce4a8e.js
deleted file mode 100644
index 797021577..000000000
Binary files a/priv/static/static/js/26.34ec129dd8f860ce4a8e.js and /dev/null differ
diff --git a/priv/static/static/js/26.34ec129dd8f860ce4a8e.js.map b/priv/static/static/js/26.34ec129dd8f860ce4a8e.js.map
deleted file mode 100644
index abff4e927..000000000
Binary files a/priv/static/static/js/26.34ec129dd8f860ce4a8e.js.map and /dev/null differ
diff --git a/priv/static/static/js/27.0f4a5145681cfb5a896e.js b/priv/static/static/js/27.0f4a5145681cfb5a896e.js
deleted file mode 100644
index 5df92f6ad..000000000
Binary files a/priv/static/static/js/27.0f4a5145681cfb5a896e.js and /dev/null differ
diff --git a/priv/static/static/js/27.0f4a5145681cfb5a896e.js.map b/priv/static/static/js/27.0f4a5145681cfb5a896e.js.map
deleted file mode 100644
index da741bf41..000000000
Binary files a/priv/static/static/js/27.0f4a5145681cfb5a896e.js.map and /dev/null differ
diff --git a/priv/static/static/js/28.75c01cd71372c39d5af8.js b/priv/static/static/js/28.75c01cd71372c39d5af8.js
deleted file mode 100644
index 63067ea18..000000000
Binary files a/priv/static/static/js/28.75c01cd71372c39d5af8.js and /dev/null differ
diff --git a/priv/static/static/js/28.75c01cd71372c39d5af8.js.map b/priv/static/static/js/28.75c01cd71372c39d5af8.js.map
deleted file mode 100644
index 4b21e788e..000000000
Binary files a/priv/static/static/js/28.75c01cd71372c39d5af8.js.map and /dev/null differ
diff --git a/priv/static/static/js/29.b53cf1f3bcece005d78a.js b/priv/static/static/js/29.b53cf1f3bcece005d78a.js
deleted file mode 100644
index 3b357be95..000000000
Binary files a/priv/static/static/js/29.b53cf1f3bcece005d78a.js and /dev/null differ
diff --git a/priv/static/static/js/29.b53cf1f3bcece005d78a.js.map b/priv/static/static/js/29.b53cf1f3bcece005d78a.js.map
deleted file mode 100644
index f3d6781f8..000000000
Binary files a/priv/static/static/js/29.b53cf1f3bcece005d78a.js.map and /dev/null differ
diff --git a/priv/static/static/js/3.bde677e65143f0cd1105.js b/priv/static/static/js/3.bde677e65143f0cd1105.js
deleted file mode 100644
index 4bea37abd..000000000
Binary files a/priv/static/static/js/3.bde677e65143f0cd1105.js and /dev/null differ
diff --git a/priv/static/static/js/3.bde677e65143f0cd1105.js.map b/priv/static/static/js/3.bde677e65143f0cd1105.js.map
deleted file mode 100644
index 06d4fc3d0..000000000
Binary files a/priv/static/static/js/3.bde677e65143f0cd1105.js.map and /dev/null differ
diff --git a/priv/static/static/js/30.064c236fa83ac21c252f.js b/priv/static/static/js/30.064c236fa83ac21c252f.js
deleted file mode 100644
index 40d81fbfd..000000000
Binary files a/priv/static/static/js/30.064c236fa83ac21c252f.js and /dev/null differ
diff --git a/priv/static/static/js/30.064c236fa83ac21c252f.js.map b/priv/static/static/js/30.064c236fa83ac21c252f.js.map
deleted file mode 100644
index 4d0d88ca9..000000000
Binary files a/priv/static/static/js/30.064c236fa83ac21c252f.js.map and /dev/null differ
diff --git a/priv/static/static/js/31.226f7a848d733df38095.js b/priv/static/static/js/31.226f7a848d733df38095.js
deleted file mode 100644
index 48131f952..000000000
Binary files a/priv/static/static/js/31.226f7a848d733df38095.js and /dev/null differ
diff --git a/priv/static/static/js/31.226f7a848d733df38095.js.map b/priv/static/static/js/31.226f7a848d733df38095.js.map
deleted file mode 100644
index 3d85d770f..000000000
Binary files a/priv/static/static/js/31.226f7a848d733df38095.js.map and /dev/null differ
diff --git a/priv/static/static/js/32.19ca50edbb4d711838dc.js b/priv/static/static/js/32.19ca50edbb4d711838dc.js
deleted file mode 100644
index 81bd5064f..000000000
Binary files a/priv/static/static/js/32.19ca50edbb4d711838dc.js and /dev/null differ
diff --git a/priv/static/static/js/32.19ca50edbb4d711838dc.js.map b/priv/static/static/js/32.19ca50edbb4d711838dc.js.map
deleted file mode 100644
index 99ad6e050..000000000
Binary files a/priv/static/static/js/32.19ca50edbb4d711838dc.js.map and /dev/null differ
diff --git a/priv/static/static/js/3733.7060d1e6bca813125a0c.js b/priv/static/static/js/3733.7060d1e6bca813125a0c.js
new file mode 100644
index 000000000..76ca488f0
Binary files /dev/null and b/priv/static/static/js/3733.7060d1e6bca813125a0c.js differ
diff --git a/priv/static/static/js/3733.7060d1e6bca813125a0c.js.LICENSE.txt b/priv/static/static/js/3733.7060d1e6bca813125a0c.js.LICENSE.txt
new file mode 100644
index 000000000..30288d49d
--- /dev/null
+++ b/priv/static/static/js/3733.7060d1e6bca813125a0c.js.LICENSE.txt
@@ -0,0 +1,38 @@
+/*!
+ localForage -- Offline Storage, Improved
+ Version 1.10.0
+ https://localforage.github.io/localForage
+ (c) 2013-2017 Mozilla, Apache License 2.0
+*/
+
+/*!
+ * devtools-if v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */
+
+/*!
+ * shared v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */
+
+/*!
+ * vue-router v4.1.6
+ * (c) 2022 Eduardo San Martin Morote
+ * @license MIT
+ */
+
+/*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */
+
+/*! (c) Andrea Giammarchi - ISC */
+
+/*! js-cookie v3.0.1 | MIT */
+
+/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
diff --git a/priv/static/static/js/3733.7060d1e6bca813125a0c.js.map b/priv/static/static/js/3733.7060d1e6bca813125a0c.js.map
new file mode 100644
index 000000000..9d6635c58
Binary files /dev/null and b/priv/static/static/js/3733.7060d1e6bca813125a0c.js.map differ
diff --git a/priv/static/static/js/4.7077bff64d63355b1635.js b/priv/static/static/js/4.7077bff64d63355b1635.js
deleted file mode 100644
index cb97d3855..000000000
Binary files a/priv/static/static/js/4.7077bff64d63355b1635.js and /dev/null differ
diff --git a/priv/static/static/js/4.7077bff64d63355b1635.js.map b/priv/static/static/js/4.7077bff64d63355b1635.js.map
deleted file mode 100644
index 83db836c8..000000000
Binary files a/priv/static/static/js/4.7077bff64d63355b1635.js.map and /dev/null differ
diff --git a/priv/static/static/js/48.b5ecdbc517423af07ca4.js b/priv/static/static/js/48.b5ecdbc517423af07ca4.js
new file mode 100644
index 000000000..cfb9ae7d1
Binary files /dev/null and b/priv/static/static/js/48.b5ecdbc517423af07ca4.js differ
diff --git a/priv/static/static/js/48.b5ecdbc517423af07ca4.js.LICENSE.txt b/priv/static/static/js/48.b5ecdbc517423af07ca4.js.LICENSE.txt
new file mode 100644
index 000000000..d6dc2a16c
--- /dev/null
+++ b/priv/static/static/js/48.b5ecdbc517423af07ca4.js.LICENSE.txt
@@ -0,0 +1,11 @@
+/*!
+ * Cropper.js v1.5.13
+ * https://fengyuanchen.github.io/cropperjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2022-11-20T05:30:46.114Z
+ */
+
+/*! vue-qrcode v2.0.0 | (c) 2018-present Chen Fengyuan | MIT */
diff --git a/priv/static/static/js/48.b5ecdbc517423af07ca4.js.map b/priv/static/static/js/48.b5ecdbc517423af07ca4.js.map
new file mode 100644
index 000000000..0693a5f08
Binary files /dev/null and b/priv/static/static/js/48.b5ecdbc517423af07ca4.js.map differ
diff --git a/priv/static/static/js/5.cfb722ac8eea8919f749.js b/priv/static/static/js/5.cfb722ac8eea8919f749.js
deleted file mode 100644
index 7d3bca163..000000000
Binary files a/priv/static/static/js/5.cfb722ac8eea8919f749.js and /dev/null differ
diff --git a/priv/static/static/js/5.cfb722ac8eea8919f749.js.map b/priv/static/static/js/5.cfb722ac8eea8919f749.js.map
deleted file mode 100644
index c9e701dc6..000000000
Binary files a/priv/static/static/js/5.cfb722ac8eea8919f749.js.map and /dev/null differ
diff --git a/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js b/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js
deleted file mode 100644
index 499d71475..000000000
Binary files a/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js and /dev/null differ
diff --git a/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js.map b/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js.map
deleted file mode 100644
index 8b78bd4b3..000000000
Binary files a/priv/static/static/js/6.613b0d6b08c3f5f9ef13.js.map and /dev/null differ
diff --git a/priv/static/static/js/6464.eb9c90a1c948cde554e9.js b/priv/static/static/js/6464.eb9c90a1c948cde554e9.js
new file mode 100644
index 000000000..28ca3ceb8
Binary files /dev/null and b/priv/static/static/js/6464.eb9c90a1c948cde554e9.js differ
diff --git a/priv/static/static/js/6464.eb9c90a1c948cde554e9.js.map b/priv/static/static/js/6464.eb9c90a1c948cde554e9.js.map
new file mode 100644
index 000000000..161864e86
Binary files /dev/null and b/priv/static/static/js/6464.eb9c90a1c948cde554e9.js.map differ
diff --git a/priv/static/static/js/7.199d52eb458f775043ed.js b/priv/static/static/js/7.199d52eb458f775043ed.js
deleted file mode 100644
index bf9015250..000000000
Binary files a/priv/static/static/js/7.199d52eb458f775043ed.js and /dev/null differ
diff --git a/priv/static/static/js/7.199d52eb458f775043ed.js.map b/priv/static/static/js/7.199d52eb458f775043ed.js.map
deleted file mode 100644
index ad860f079..000000000
Binary files a/priv/static/static/js/7.199d52eb458f775043ed.js.map and /dev/null differ
diff --git a/priv/static/static/js/7586.981b2305a0019f6042a5.js b/priv/static/static/js/7586.981b2305a0019f6042a5.js
new file mode 100644
index 000000000..ea48cced4
Binary files /dev/null and b/priv/static/static/js/7586.981b2305a0019f6042a5.js differ
diff --git a/priv/static/static/js/7586.981b2305a0019f6042a5.js.map b/priv/static/static/js/7586.981b2305a0019f6042a5.js.map
new file mode 100644
index 000000000..8795927ac
Binary files /dev/null and b/priv/static/static/js/7586.981b2305a0019f6042a5.js.map differ
diff --git a/priv/static/static/js/7962.e25d40b042f8ee7389c3.js b/priv/static/static/js/7962.e25d40b042f8ee7389c3.js
new file mode 100644
index 000000000..aa740878b
Binary files /dev/null and b/priv/static/static/js/7962.e25d40b042f8ee7389c3.js differ
diff --git a/priv/static/static/js/7962.e25d40b042f8ee7389c3.js.map b/priv/static/static/js/7962.e25d40b042f8ee7389c3.js.map
new file mode 100644
index 000000000..cbe1b1bf3
Binary files /dev/null and b/priv/static/static/js/7962.e25d40b042f8ee7389c3.js.map differ
diff --git a/priv/static/static/js/8.7f96f22f9f65ad394684.js b/priv/static/static/js/8.7f96f22f9f65ad394684.js
deleted file mode 100644
index 154e63437..000000000
Binary files a/priv/static/static/js/8.7f96f22f9f65ad394684.js and /dev/null differ
diff --git a/priv/static/static/js/8.7f96f22f9f65ad394684.js.map b/priv/static/static/js/8.7f96f22f9f65ad394684.js.map
deleted file mode 100644
index 74e510286..000000000
Binary files a/priv/static/static/js/8.7f96f22f9f65ad394684.js.map and /dev/null differ
diff --git a/priv/static/static/js/9.f8fc2497d5f27a9df682.js b/priv/static/static/js/9.f8fc2497d5f27a9df682.js
deleted file mode 100644
index c86ae4d9a..000000000
Binary files a/priv/static/static/js/9.f8fc2497d5f27a9df682.js and /dev/null differ
diff --git a/priv/static/static/js/9.f8fc2497d5f27a9df682.js.map b/priv/static/static/js/9.f8fc2497d5f27a9df682.js.map
deleted file mode 100644
index 50ff032de..000000000
Binary files a/priv/static/static/js/9.f8fc2497d5f27a9df682.js.map and /dev/null differ
diff --git a/priv/static/static/js/9060.24271e167e0471a1a732.js b/priv/static/static/js/9060.24271e167e0471a1a732.js
new file mode 100644
index 000000000..2113b7bcb
Binary files /dev/null and b/priv/static/static/js/9060.24271e167e0471a1a732.js differ
diff --git a/priv/static/static/js/9060.24271e167e0471a1a732.js.map b/priv/static/static/js/9060.24271e167e0471a1a732.js.map
new file mode 100644
index 000000000..86061615f
Binary files /dev/null and b/priv/static/static/js/9060.24271e167e0471a1a732.js.map differ
diff --git a/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js b/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js
new file mode 100644
index 000000000..b96ffb7f0
Binary files /dev/null and b/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js differ
diff --git a/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js.map b/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js.map
new file mode 100644
index 000000000..5529600fb
Binary files /dev/null and b/priv/static/static/js/9801.99ace6b5dc657bf1a65b.js.map differ
diff --git a/priv/static/static/js/app.6c972d84b60f601b01f8.js b/priv/static/static/js/app.6c972d84b60f601b01f8.js
deleted file mode 100644
index f00f10017..000000000
Binary files a/priv/static/static/js/app.6c972d84b60f601b01f8.js and /dev/null differ
diff --git a/priv/static/static/js/app.6c972d84b60f601b01f8.js.map b/priv/static/static/js/app.6c972d84b60f601b01f8.js.map
deleted file mode 100644
index 2e5c2bd67..000000000
Binary files a/priv/static/static/js/app.6c972d84b60f601b01f8.js.map and /dev/null differ
diff --git a/priv/static/static/js/app.7c4b412b26221a7c8572.js b/priv/static/static/js/app.7c4b412b26221a7c8572.js
new file mode 100644
index 000000000..d48c8194f
Binary files /dev/null and b/priv/static/static/js/app.7c4b412b26221a7c8572.js differ
diff --git a/priv/static/static/js/app.7c4b412b26221a7c8572.js.map b/priv/static/static/js/app.7c4b412b26221a7c8572.js.map
new file mode 100644
index 000000000..a21005e3c
Binary files /dev/null and b/priv/static/static/js/app.7c4b412b26221a7c8572.js.map differ
diff --git a/priv/static/static/js/emoji-annotations/af-json.96c988285a6a2cce6246.js b/priv/static/static/js/emoji-annotations/af-json.96c988285a6a2cce6246.js
new file mode 100644
index 000000000..9ab40ae17
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/af-json.96c988285a6a2cce6246.js differ
diff --git a/priv/static/static/js/emoji-annotations/am-json.94388548f7c18233fec3.js b/priv/static/static/js/emoji-annotations/am-json.94388548f7c18233fec3.js
new file mode 100644
index 000000000..26f3e4ee8
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/am-json.94388548f7c18233fec3.js differ
diff --git a/priv/static/static/js/emoji-annotations/ar-json.5527466d349f2954d49b.js b/priv/static/static/js/emoji-annotations/ar-json.5527466d349f2954d49b.js
new file mode 100644
index 000000000..ab7b2359a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ar-json.5527466d349f2954d49b.js differ
diff --git a/priv/static/static/js/emoji-annotations/ar_SA-json.4a6f4402931a867070f0.js b/priv/static/static/js/emoji-annotations/ar_SA-json.4a6f4402931a867070f0.js
new file mode 100644
index 000000000..c46514948
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ar_SA-json.4a6f4402931a867070f0.js differ
diff --git a/priv/static/static/js/emoji-annotations/as-json.34cf67edc1cecd195738.js b/priv/static/static/js/emoji-annotations/as-json.34cf67edc1cecd195738.js
new file mode 100644
index 000000000..fba4c45d3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/as-json.34cf67edc1cecd195738.js differ
diff --git a/priv/static/static/js/emoji-annotations/ast-json.7e4da8cb7e539c19a9b9.js b/priv/static/static/js/emoji-annotations/ast-json.7e4da8cb7e539c19a9b9.js
new file mode 100644
index 000000000..5c461f83f
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ast-json.7e4da8cb7e539c19a9b9.js differ
diff --git a/priv/static/static/js/emoji-annotations/az-json.9a32f2941d9c4d1f834c.js b/priv/static/static/js/emoji-annotations/az-json.9a32f2941d9c4d1f834c.js
new file mode 100644
index 000000000..0fe7c8951
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/az-json.9a32f2941d9c4d1f834c.js differ
diff --git a/priv/static/static/js/emoji-annotations/be-json.f92f80669873a4100c69.js b/priv/static/static/js/emoji-annotations/be-json.f92f80669873a4100c69.js
new file mode 100644
index 000000000..919a4a46e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/be-json.f92f80669873a4100c69.js differ
diff --git a/priv/static/static/js/emoji-annotations/bg-json.7c69c52572a7bf87e1db.js b/priv/static/static/js/emoji-annotations/bg-json.7c69c52572a7bf87e1db.js
new file mode 100644
index 000000000..2ab93affa
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/bg-json.7c69c52572a7bf87e1db.js differ
diff --git a/priv/static/static/js/emoji-annotations/bn-json.657aac057f36ad06c58d.js b/priv/static/static/js/emoji-annotations/bn-json.657aac057f36ad06c58d.js
new file mode 100644
index 000000000..60760a5a4
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/bn-json.657aac057f36ad06c58d.js differ
diff --git a/priv/static/static/js/emoji-annotations/br-json.c7175423d8965ed10bae.js b/priv/static/static/js/emoji-annotations/br-json.c7175423d8965ed10bae.js
new file mode 100644
index 000000000..f3ffa6b97
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/br-json.c7175423d8965ed10bae.js differ
diff --git a/priv/static/static/js/emoji-annotations/bs-json.48ef42da1c7976cf083c.js b/priv/static/static/js/emoji-annotations/bs-json.48ef42da1c7976cf083c.js
new file mode 100644
index 000000000..e00af65a9
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/bs-json.48ef42da1c7976cf083c.js differ
diff --git a/priv/static/static/js/emoji-annotations/ca-json.ce029e860b10b242c6a5.js b/priv/static/static/js/emoji-annotations/ca-json.ce029e860b10b242c6a5.js
new file mode 100644
index 000000000..f550915b4
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ca-json.ce029e860b10b242c6a5.js differ
diff --git a/priv/static/static/js/emoji-annotations/ccp-json.02836537ffe2b02291cb.js b/priv/static/static/js/emoji-annotations/ccp-json.02836537ffe2b02291cb.js
new file mode 100644
index 000000000..388f5acd5
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ccp-json.02836537ffe2b02291cb.js differ
diff --git a/priv/static/static/js/emoji-annotations/ceb-json.9db0336f168aa77ee395.js b/priv/static/static/js/emoji-annotations/ceb-json.9db0336f168aa77ee395.js
new file mode 100644
index 000000000..b93f00852
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ceb-json.9db0336f168aa77ee395.js differ
diff --git a/priv/static/static/js/emoji-annotations/chr-json.e7e542ab5a74167dec10.js b/priv/static/static/js/emoji-annotations/chr-json.e7e542ab5a74167dec10.js
new file mode 100644
index 000000000..e0d41f2ea
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/chr-json.e7e542ab5a74167dec10.js differ
diff --git a/priv/static/static/js/emoji-annotations/ckb-json.13dc83db1e15aa76051a.js b/priv/static/static/js/emoji-annotations/ckb-json.13dc83db1e15aa76051a.js
new file mode 100644
index 000000000..71e424fb2
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ckb-json.13dc83db1e15aa76051a.js differ
diff --git a/priv/static/static/js/emoji-annotations/cs-json.48d8bba230dc9fe6b3dc.js b/priv/static/static/js/emoji-annotations/cs-json.48d8bba230dc9fe6b3dc.js
new file mode 100644
index 000000000..5fea830f7
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/cs-json.48d8bba230dc9fe6b3dc.js differ
diff --git a/priv/static/static/js/emoji-annotations/cy-json.e09dd13da5ad56530ead.js b/priv/static/static/js/emoji-annotations/cy-json.e09dd13da5ad56530ead.js
new file mode 100644
index 000000000..f25fb64af
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/cy-json.e09dd13da5ad56530ead.js differ
diff --git a/priv/static/static/js/emoji-annotations/da-json.8d074e27df71edafc543.js b/priv/static/static/js/emoji-annotations/da-json.8d074e27df71edafc543.js
new file mode 100644
index 000000000..b02e48ba2
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/da-json.8d074e27df71edafc543.js differ
diff --git a/priv/static/static/js/emoji-annotations/de-json.e1443c01a191af1665e1.js b/priv/static/static/js/emoji-annotations/de-json.e1443c01a191af1665e1.js
new file mode 100644
index 000000000..e097a687e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/de-json.e1443c01a191af1665e1.js differ
diff --git a/priv/static/static/js/emoji-annotations/de_CH-json.b4a5891ea2f38d616ec0.js b/priv/static/static/js/emoji-annotations/de_CH-json.b4a5891ea2f38d616ec0.js
new file mode 100644
index 000000000..d65a8edc0
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/de_CH-json.b4a5891ea2f38d616ec0.js differ
diff --git a/priv/static/static/js/emoji-annotations/doi-json.7841c3f3ceb4e3da0bd1.js b/priv/static/static/js/emoji-annotations/doi-json.7841c3f3ceb4e3da0bd1.js
new file mode 100644
index 000000000..a7abeee85
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/doi-json.7841c3f3ceb4e3da0bd1.js differ
diff --git a/priv/static/static/js/emoji-annotations/dsb-json.7635686ffd8d62264466.js b/priv/static/static/js/emoji-annotations/dsb-json.7635686ffd8d62264466.js
new file mode 100644
index 000000000..8fb1d3632
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/dsb-json.7635686ffd8d62264466.js differ
diff --git a/priv/static/static/js/emoji-annotations/el-json.b36610f2ea16d56c2314.js b/priv/static/static/js/emoji-annotations/el-json.b36610f2ea16d56c2314.js
new file mode 100644
index 000000000..dd5ae7fd8
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/el-json.b36610f2ea16d56c2314.js differ
diff --git a/priv/static/static/js/emoji-annotations/en-json.6c3947f7c49c3952084d.js b/priv/static/static/js/emoji-annotations/en-json.6c3947f7c49c3952084d.js
new file mode 100644
index 000000000..3787c96c9
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en-json.6c3947f7c49c3952084d.js differ
diff --git a/priv/static/static/js/emoji-annotations/en_001-json.72d8c47269350f59aa9f.js b/priv/static/static/js/emoji-annotations/en_001-json.72d8c47269350f59aa9f.js
new file mode 100644
index 000000000..186874caf
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en_001-json.72d8c47269350f59aa9f.js differ
diff --git a/priv/static/static/js/emoji-annotations/en_AU-json.33aac9bbd887273a34b1.js b/priv/static/static/js/emoji-annotations/en_AU-json.33aac9bbd887273a34b1.js
new file mode 100644
index 000000000..f4b98d214
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en_AU-json.33aac9bbd887273a34b1.js differ
diff --git a/priv/static/static/js/emoji-annotations/en_CA-json.5de965778a6b8a5a4bb4.js b/priv/static/static/js/emoji-annotations/en_CA-json.5de965778a6b8a5a4bb4.js
new file mode 100644
index 000000000..0b3e2e7b2
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en_CA-json.5de965778a6b8a5a4bb4.js differ
diff --git a/priv/static/static/js/emoji-annotations/en_GB-json.bd687f904492facc81d1.js b/priv/static/static/js/emoji-annotations/en_GB-json.bd687f904492facc81d1.js
new file mode 100644
index 000000000..ff1483ed7
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en_GB-json.bd687f904492facc81d1.js differ
diff --git a/priv/static/static/js/emoji-annotations/en_IN-json.002faa48c09121928fca.js b/priv/static/static/js/emoji-annotations/en_IN-json.002faa48c09121928fca.js
new file mode 100644
index 000000000..4ff67b066
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/en_IN-json.002faa48c09121928fca.js differ
diff --git a/priv/static/static/js/emoji-annotations/es-json.f593b0dc2367a9d7fb30.js b/priv/static/static/js/emoji-annotations/es-json.f593b0dc2367a9d7fb30.js
new file mode 100644
index 000000000..cb226229b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/es-json.f593b0dc2367a9d7fb30.js differ
diff --git a/priv/static/static/js/emoji-annotations/es_419-json.d81991295392b6ed83bb.js b/priv/static/static/js/emoji-annotations/es_419-json.d81991295392b6ed83bb.js
new file mode 100644
index 000000000..17bac2ddf
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/es_419-json.d81991295392b6ed83bb.js differ
diff --git a/priv/static/static/js/emoji-annotations/es_MX-json.ee359d4b611fdb1aeb33.js b/priv/static/static/js/emoji-annotations/es_MX-json.ee359d4b611fdb1aeb33.js
new file mode 100644
index 000000000..a278ed5da
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/es_MX-json.ee359d4b611fdb1aeb33.js differ
diff --git a/priv/static/static/js/emoji-annotations/es_US-json.280bdb036dfd651d079a.js b/priv/static/static/js/emoji-annotations/es_US-json.280bdb036dfd651d079a.js
new file mode 100644
index 000000000..a5f8aeab7
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/es_US-json.280bdb036dfd651d079a.js differ
diff --git a/priv/static/static/js/emoji-annotations/et-json.c42e3f186a55ecb061cd.js b/priv/static/static/js/emoji-annotations/et-json.c42e3f186a55ecb061cd.js
new file mode 100644
index 000000000..803f7f24b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/et-json.c42e3f186a55ecb061cd.js differ
diff --git a/priv/static/static/js/emoji-annotations/eu-json.931b429f5fcc141549a5.js b/priv/static/static/js/emoji-annotations/eu-json.931b429f5fcc141549a5.js
new file mode 100644
index 000000000..8a37fda0b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/eu-json.931b429f5fcc141549a5.js differ
diff --git a/priv/static/static/js/emoji-annotations/fa-json.819c7f263c8594ccf4fa.js b/priv/static/static/js/emoji-annotations/fa-json.819c7f263c8594ccf4fa.js
new file mode 100644
index 000000000..0a5a57d89
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fa-json.819c7f263c8594ccf4fa.js differ
diff --git a/priv/static/static/js/emoji-annotations/fi-json.0f254bb4b0faaba4abcc.js b/priv/static/static/js/emoji-annotations/fi-json.0f254bb4b0faaba4abcc.js
new file mode 100644
index 000000000..dd31d0991
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fi-json.0f254bb4b0faaba4abcc.js differ
diff --git a/priv/static/static/js/emoji-annotations/fil-json.4fca833f178d1b889b69.js b/priv/static/static/js/emoji-annotations/fil-json.4fca833f178d1b889b69.js
new file mode 100644
index 000000000..b1b6fdab2
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fil-json.4fca833f178d1b889b69.js differ
diff --git a/priv/static/static/js/emoji-annotations/fo-json.9b060e8009b3a8be4597.js b/priv/static/static/js/emoji-annotations/fo-json.9b060e8009b3a8be4597.js
new file mode 100644
index 000000000..50ce60362
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fo-json.9b060e8009b3a8be4597.js differ
diff --git a/priv/static/static/js/emoji-annotations/fr-json.da49ed0d1a6622fe1c67.js b/priv/static/static/js/emoji-annotations/fr-json.da49ed0d1a6622fe1c67.js
new file mode 100644
index 000000000..024a6b501
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fr-json.da49ed0d1a6622fe1c67.js differ
diff --git a/priv/static/static/js/emoji-annotations/fr_CA-json.59ed6b59e2ca68707292.js b/priv/static/static/js/emoji-annotations/fr_CA-json.59ed6b59e2ca68707292.js
new file mode 100644
index 000000000..f984d54fc
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/fr_CA-json.59ed6b59e2ca68707292.js differ
diff --git a/priv/static/static/js/emoji-annotations/ga-json.cb3f9e613a8c445aea23.js b/priv/static/static/js/emoji-annotations/ga-json.cb3f9e613a8c445aea23.js
new file mode 100644
index 000000000..0308ffe78
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ga-json.cb3f9e613a8c445aea23.js differ
diff --git a/priv/static/static/js/emoji-annotations/gd-json.e3d0aea3725be774ad81.js b/priv/static/static/js/emoji-annotations/gd-json.e3d0aea3725be774ad81.js
new file mode 100644
index 000000000..c298402c3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/gd-json.e3d0aea3725be774ad81.js differ
diff --git a/priv/static/static/js/emoji-annotations/gl-json.ce89036f0ae72224c994.js b/priv/static/static/js/emoji-annotations/gl-json.ce89036f0ae72224c994.js
new file mode 100644
index 000000000..e8876b636
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/gl-json.ce89036f0ae72224c994.js differ
diff --git a/priv/static/static/js/emoji-annotations/gu-json.8a899f364cf260376905.js b/priv/static/static/js/emoji-annotations/gu-json.8a899f364cf260376905.js
new file mode 100644
index 000000000..3be3d2a21
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/gu-json.8a899f364cf260376905.js differ
diff --git a/priv/static/static/js/emoji-annotations/ha-json.2a08912b38925c10f970.js b/priv/static/static/js/emoji-annotations/ha-json.2a08912b38925c10f970.js
new file mode 100644
index 000000000..5bd431007
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ha-json.2a08912b38925c10f970.js differ
diff --git a/priv/static/static/js/emoji-annotations/ha_NE-json.e4855d92aaccfdd6ba57.js b/priv/static/static/js/emoji-annotations/ha_NE-json.e4855d92aaccfdd6ba57.js
new file mode 100644
index 000000000..d879e2f1f
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ha_NE-json.e4855d92aaccfdd6ba57.js differ
diff --git a/priv/static/static/js/emoji-annotations/he-json.a3d7631f32182b0955a2.js b/priv/static/static/js/emoji-annotations/he-json.a3d7631f32182b0955a2.js
new file mode 100644
index 000000000..0fc14d50b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/he-json.a3d7631f32182b0955a2.js differ
diff --git a/priv/static/static/js/emoji-annotations/hi-json.04bc5f73dc2169def97e.js b/priv/static/static/js/emoji-annotations/hi-json.04bc5f73dc2169def97e.js
new file mode 100644
index 000000000..cf395d738
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hi-json.04bc5f73dc2169def97e.js differ
diff --git a/priv/static/static/js/emoji-annotations/hi_Latn-json.3cb73c456f31261f1908.js b/priv/static/static/js/emoji-annotations/hi_Latn-json.3cb73c456f31261f1908.js
new file mode 100644
index 000000000..c82838227
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hi_Latn-json.3cb73c456f31261f1908.js differ
diff --git a/priv/static/static/js/emoji-annotations/hr-json.fe847ade1f18a60e513c.js b/priv/static/static/js/emoji-annotations/hr-json.fe847ade1f18a60e513c.js
new file mode 100644
index 000000000..7701a3d40
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hr-json.fe847ade1f18a60e513c.js differ
diff --git a/priv/static/static/js/emoji-annotations/hsb-json.438721731b4171bc6fc3.js b/priv/static/static/js/emoji-annotations/hsb-json.438721731b4171bc6fc3.js
new file mode 100644
index 000000000..e3db84acb
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hsb-json.438721731b4171bc6fc3.js differ
diff --git a/priv/static/static/js/emoji-annotations/hu-json.1faf52040deda872b416.js b/priv/static/static/js/emoji-annotations/hu-json.1faf52040deda872b416.js
new file mode 100644
index 000000000..f6c8a153c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hu-json.1faf52040deda872b416.js differ
diff --git a/priv/static/static/js/emoji-annotations/hy-json.2d819f7faabfeba8457f.js b/priv/static/static/js/emoji-annotations/hy-json.2d819f7faabfeba8457f.js
new file mode 100644
index 000000000..316c45977
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/hy-json.2d819f7faabfeba8457f.js differ
diff --git a/priv/static/static/js/emoji-annotations/ia-json.8e8365e2bf41779e2beb.js b/priv/static/static/js/emoji-annotations/ia-json.8e8365e2bf41779e2beb.js
new file mode 100644
index 000000000..986aa85cb
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ia-json.8e8365e2bf41779e2beb.js differ
diff --git a/priv/static/static/js/emoji-annotations/id-json.55f83c46d753b0b69330.js b/priv/static/static/js/emoji-annotations/id-json.55f83c46d753b0b69330.js
new file mode 100644
index 000000000..a765acd3e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/id-json.55f83c46d753b0b69330.js differ
diff --git a/priv/static/static/js/emoji-annotations/ig-json.f19fa5fe3582463ba73c.js b/priv/static/static/js/emoji-annotations/ig-json.f19fa5fe3582463ba73c.js
new file mode 100644
index 000000000..f2f94eee4
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ig-json.f19fa5fe3582463ba73c.js differ
diff --git a/priv/static/static/js/emoji-annotations/is-json.62e220c65215b034533d.js b/priv/static/static/js/emoji-annotations/is-json.62e220c65215b034533d.js
new file mode 100644
index 000000000..890ce1c84
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/is-json.62e220c65215b034533d.js differ
diff --git a/priv/static/static/js/emoji-annotations/it-json.9c1758f4fd3391f7f61a.js b/priv/static/static/js/emoji-annotations/it-json.9c1758f4fd3391f7f61a.js
new file mode 100644
index 000000000..aa0615fdd
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/it-json.9c1758f4fd3391f7f61a.js differ
diff --git a/priv/static/static/js/emoji-annotations/ja-json.0bd51f8c40c106355b0e.js b/priv/static/static/js/emoji-annotations/ja-json.0bd51f8c40c106355b0e.js
new file mode 100644
index 000000000..1b042732c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ja-json.0bd51f8c40c106355b0e.js differ
diff --git a/priv/static/static/js/emoji-annotations/jv-json.1c8d5ffcff22b46b6214.js b/priv/static/static/js/emoji-annotations/jv-json.1c8d5ffcff22b46b6214.js
new file mode 100644
index 000000000..e560e315a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/jv-json.1c8d5ffcff22b46b6214.js differ
diff --git a/priv/static/static/js/emoji-annotations/ka-json.567e7b051d90a37003f3.js b/priv/static/static/js/emoji-annotations/ka-json.567e7b051d90a37003f3.js
new file mode 100644
index 000000000..2e8e0cfbe
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ka-json.567e7b051d90a37003f3.js differ
diff --git a/priv/static/static/js/emoji-annotations/kab-json.1a2de4774f4ddc2b51dd.js b/priv/static/static/js/emoji-annotations/kab-json.1a2de4774f4ddc2b51dd.js
new file mode 100644
index 000000000..ae1ca4e58
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/kab-json.1a2de4774f4ddc2b51dd.js differ
diff --git a/priv/static/static/js/emoji-annotations/kk-json.a20b59d47bdfe99786ad.js b/priv/static/static/js/emoji-annotations/kk-json.a20b59d47bdfe99786ad.js
new file mode 100644
index 000000000..e61af43a2
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/kk-json.a20b59d47bdfe99786ad.js differ
diff --git a/priv/static/static/js/emoji-annotations/kl-json.87ab8661b4bdecd09faf.js b/priv/static/static/js/emoji-annotations/kl-json.87ab8661b4bdecd09faf.js
new file mode 100644
index 000000000..8d444173a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/kl-json.87ab8661b4bdecd09faf.js differ
diff --git a/priv/static/static/js/emoji-annotations/km-json.9bc922b2e0faa64b4c53.js b/priv/static/static/js/emoji-annotations/km-json.9bc922b2e0faa64b4c53.js
new file mode 100644
index 000000000..f79dedf5b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/km-json.9bc922b2e0faa64b4c53.js differ
diff --git a/priv/static/static/js/emoji-annotations/kn-json.efdac8ac0cb00991ba1e.js b/priv/static/static/js/emoji-annotations/kn-json.efdac8ac0cb00991ba1e.js
new file mode 100644
index 000000000..a1eaf8e32
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/kn-json.efdac8ac0cb00991ba1e.js differ
diff --git a/priv/static/static/js/emoji-annotations/ko-json.d175900fe48f48ce87c8.js b/priv/static/static/js/emoji-annotations/ko-json.d175900fe48f48ce87c8.js
new file mode 100644
index 000000000..718c0c2b5
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ko-json.d175900fe48f48ce87c8.js differ
diff --git a/priv/static/static/js/emoji-annotations/kok-json.116e4f72db3bfb846233.js b/priv/static/static/js/emoji-annotations/kok-json.116e4f72db3bfb846233.js
new file mode 100644
index 000000000..9ab42f0af
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/kok-json.116e4f72db3bfb846233.js differ
diff --git a/priv/static/static/js/emoji-annotations/ku-json.2173ed87f8d7372ee209.js b/priv/static/static/js/emoji-annotations/ku-json.2173ed87f8d7372ee209.js
new file mode 100644
index 000000000..1a17c8649
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ku-json.2173ed87f8d7372ee209.js differ
diff --git a/priv/static/static/js/emoji-annotations/ky-json.310bda579c819eda9472.js b/priv/static/static/js/emoji-annotations/ky-json.310bda579c819eda9472.js
new file mode 100644
index 000000000..88b089f37
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ky-json.310bda579c819eda9472.js differ
diff --git a/priv/static/static/js/emoji-annotations/lb-json.499c526f3a653618ea9b.js b/priv/static/static/js/emoji-annotations/lb-json.499c526f3a653618ea9b.js
new file mode 100644
index 000000000..2bd89d552
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/lb-json.499c526f3a653618ea9b.js differ
diff --git a/priv/static/static/js/emoji-annotations/lo-json.ecb06d61465a355b8157.js b/priv/static/static/js/emoji-annotations/lo-json.ecb06d61465a355b8157.js
new file mode 100644
index 000000000..8c483721c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/lo-json.ecb06d61465a355b8157.js differ
diff --git a/priv/static/static/js/emoji-annotations/lt-json.ced4d5e70edc60127df6.js b/priv/static/static/js/emoji-annotations/lt-json.ced4d5e70edc60127df6.js
new file mode 100644
index 000000000..059ad5d83
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/lt-json.ced4d5e70edc60127df6.js differ
diff --git a/priv/static/static/js/emoji-annotations/lv-json.5600c2eb6d59fa0aa2e2.js b/priv/static/static/js/emoji-annotations/lv-json.5600c2eb6d59fa0aa2e2.js
new file mode 100644
index 000000000..d1ca388ed
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/lv-json.5600c2eb6d59fa0aa2e2.js differ
diff --git a/priv/static/static/js/emoji-annotations/mai-json.302386b3358f4d34f9c8.js b/priv/static/static/js/emoji-annotations/mai-json.302386b3358f4d34f9c8.js
new file mode 100644
index 000000000..5327acaa3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mai-json.302386b3358f4d34f9c8.js differ
diff --git a/priv/static/static/js/emoji-annotations/mi-json.9efe6f146ecd8987f80a.js b/priv/static/static/js/emoji-annotations/mi-json.9efe6f146ecd8987f80a.js
new file mode 100644
index 000000000..f1a11c06a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mi-json.9efe6f146ecd8987f80a.js differ
diff --git a/priv/static/static/js/emoji-annotations/mk-json.a254cabb8570419cc426.js b/priv/static/static/js/emoji-annotations/mk-json.a254cabb8570419cc426.js
new file mode 100644
index 000000000..aa4231d84
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mk-json.a254cabb8570419cc426.js differ
diff --git a/priv/static/static/js/emoji-annotations/ml-json.3f2902a84240faff1b48.js b/priv/static/static/js/emoji-annotations/ml-json.3f2902a84240faff1b48.js
new file mode 100644
index 000000000..dc10a1b57
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ml-json.3f2902a84240faff1b48.js differ
diff --git a/priv/static/static/js/emoji-annotations/mn-json.764169f1168d0432640f.js b/priv/static/static/js/emoji-annotations/mn-json.764169f1168d0432640f.js
new file mode 100644
index 000000000..6ad3a6e88
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mn-json.764169f1168d0432640f.js differ
diff --git a/priv/static/static/js/emoji-annotations/mni-json.7db7f76ab1ce34e3683d.js b/priv/static/static/js/emoji-annotations/mni-json.7db7f76ab1ce34e3683d.js
new file mode 100644
index 000000000..4f295e230
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mni-json.7db7f76ab1ce34e3683d.js differ
diff --git a/priv/static/static/js/emoji-annotations/mr-json.cb0a87d9aabf8a52161d.js b/priv/static/static/js/emoji-annotations/mr-json.cb0a87d9aabf8a52161d.js
new file mode 100644
index 000000000..6aaca24cc
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mr-json.cb0a87d9aabf8a52161d.js differ
diff --git a/priv/static/static/js/emoji-annotations/ms-json.272ee4735aabc37015dd.js b/priv/static/static/js/emoji-annotations/ms-json.272ee4735aabc37015dd.js
new file mode 100644
index 000000000..3e3b2262c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ms-json.272ee4735aabc37015dd.js differ
diff --git a/priv/static/static/js/emoji-annotations/mt-json.3ddf7dbc114adf90c500.js b/priv/static/static/js/emoji-annotations/mt-json.3ddf7dbc114adf90c500.js
new file mode 100644
index 000000000..ce92cdd20
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/mt-json.3ddf7dbc114adf90c500.js differ
diff --git a/priv/static/static/js/emoji-annotations/my-json.8e7dd1485813d15bba7e.js b/priv/static/static/js/emoji-annotations/my-json.8e7dd1485813d15bba7e.js
new file mode 100644
index 000000000..c0e7cb93d
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/my-json.8e7dd1485813d15bba7e.js differ
diff --git a/priv/static/static/js/emoji-annotations/ne-json.a0118dca2096a101b8e8.js b/priv/static/static/js/emoji-annotations/ne-json.a0118dca2096a101b8e8.js
new file mode 100644
index 000000000..a055cf0b1
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ne-json.a0118dca2096a101b8e8.js differ
diff --git a/priv/static/static/js/emoji-annotations/nl-json.52f4b93b8fa5e22cb585.js b/priv/static/static/js/emoji-annotations/nl-json.52f4b93b8fa5e22cb585.js
new file mode 100644
index 000000000..8fdb6f28d
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/nl-json.52f4b93b8fa5e22cb585.js differ
diff --git a/priv/static/static/js/emoji-annotations/nn-json.7293cd5d7205681cf48c.js b/priv/static/static/js/emoji-annotations/nn-json.7293cd5d7205681cf48c.js
new file mode 100644
index 000000000..a66e8257e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/nn-json.7293cd5d7205681cf48c.js differ
diff --git a/priv/static/static/js/emoji-annotations/no-json.22bdbbc77cc3c14ada58.js b/priv/static/static/js/emoji-annotations/no-json.22bdbbc77cc3c14ada58.js
new file mode 100644
index 000000000..a8f2a425e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/no-json.22bdbbc77cc3c14ada58.js differ
diff --git a/priv/static/static/js/emoji-annotations/or-json.b007c2f6f92dcc95efac.js b/priv/static/static/js/emoji-annotations/or-json.b007c2f6f92dcc95efac.js
new file mode 100644
index 000000000..fe769e9d5
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/or-json.b007c2f6f92dcc95efac.js differ
diff --git a/priv/static/static/js/emoji-annotations/pa-json.7d60d69762a108270669.js b/priv/static/static/js/emoji-annotations/pa-json.7d60d69762a108270669.js
new file mode 100644
index 000000000..5fae213cd
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pa-json.7d60d69762a108270669.js differ
diff --git a/priv/static/static/js/emoji-annotations/pa_Arab-json.5ae021308b1c5f6dd8a7.js b/priv/static/static/js/emoji-annotations/pa_Arab-json.5ae021308b1c5f6dd8a7.js
new file mode 100644
index 000000000..0e7bfc042
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pa_Arab-json.5ae021308b1c5f6dd8a7.js differ
diff --git a/priv/static/static/js/emoji-annotations/pcm-json.eac3a5ad0b4b5b33289d.js b/priv/static/static/js/emoji-annotations/pcm-json.eac3a5ad0b4b5b33289d.js
new file mode 100644
index 000000000..6bb5fa7c9
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pcm-json.eac3a5ad0b4b5b33289d.js differ
diff --git a/priv/static/static/js/emoji-annotations/pl-json.e9e8f1ed84dc6c169516.js b/priv/static/static/js/emoji-annotations/pl-json.e9e8f1ed84dc6c169516.js
new file mode 100644
index 000000000..3956d3c5b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pl-json.e9e8f1ed84dc6c169516.js differ
diff --git a/priv/static/static/js/emoji-annotations/ps-json.a0d9eea5b81bcad11e64.js b/priv/static/static/js/emoji-annotations/ps-json.a0d9eea5b81bcad11e64.js
new file mode 100644
index 000000000..b6c3e9d66
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ps-json.a0d9eea5b81bcad11e64.js differ
diff --git a/priv/static/static/js/emoji-annotations/pt-json.2b2512ee44291bdb2ae7.js b/priv/static/static/js/emoji-annotations/pt-json.2b2512ee44291bdb2ae7.js
new file mode 100644
index 000000000..1a7176b73
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pt-json.2b2512ee44291bdb2ae7.js differ
diff --git a/priv/static/static/js/emoji-annotations/pt_PT-json.c381b13e323f91b0e6b3.js b/priv/static/static/js/emoji-annotations/pt_PT-json.c381b13e323f91b0e6b3.js
new file mode 100644
index 000000000..477aee3cb
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/pt_PT-json.c381b13e323f91b0e6b3.js differ
diff --git a/priv/static/static/js/emoji-annotations/qu-json.55f4df57e71076dbad9f.js b/priv/static/static/js/emoji-annotations/qu-json.55f4df57e71076dbad9f.js
new file mode 100644
index 000000000..4393845a3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/qu-json.55f4df57e71076dbad9f.js differ
diff --git a/priv/static/static/js/emoji-annotations/rm-json.985d4934f386fd05a75c.js b/priv/static/static/js/emoji-annotations/rm-json.985d4934f386fd05a75c.js
new file mode 100644
index 000000000..40d320ce3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/rm-json.985d4934f386fd05a75c.js differ
diff --git a/priv/static/static/js/emoji-annotations/ro-json.cb62f86ce78c94d1e813.js b/priv/static/static/js/emoji-annotations/ro-json.cb62f86ce78c94d1e813.js
new file mode 100644
index 000000000..be57a3e26
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ro-json.cb62f86ce78c94d1e813.js differ
diff --git a/priv/static/static/js/emoji-annotations/ru-json.a81d0df34460837ccacc.js b/priv/static/static/js/emoji-annotations/ru-json.a81d0df34460837ccacc.js
new file mode 100644
index 000000000..45d01cf80
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ru-json.a81d0df34460837ccacc.js differ
diff --git a/priv/static/static/js/emoji-annotations/rw-json.031838c5374676191131.js b/priv/static/static/js/emoji-annotations/rw-json.031838c5374676191131.js
new file mode 100644
index 000000000..78a3ec41a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/rw-json.031838c5374676191131.js differ
diff --git a/priv/static/static/js/emoji-annotations/sa-json.21b06234a08c7469ccb4.js b/priv/static/static/js/emoji-annotations/sa-json.21b06234a08c7469ccb4.js
new file mode 100644
index 000000000..168c85b93
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sa-json.21b06234a08c7469ccb4.js differ
diff --git a/priv/static/static/js/emoji-annotations/sat-json.71e92700b2aaca8e021c.js b/priv/static/static/js/emoji-annotations/sat-json.71e92700b2aaca8e021c.js
new file mode 100644
index 000000000..d1e87b289
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sat-json.71e92700b2aaca8e021c.js differ
diff --git a/priv/static/static/js/emoji-annotations/sc-json.c84c9c47d2e104c43e4c.js b/priv/static/static/js/emoji-annotations/sc-json.c84c9c47d2e104c43e4c.js
new file mode 100644
index 000000000..92f43d81b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sc-json.c84c9c47d2e104c43e4c.js differ
diff --git a/priv/static/static/js/emoji-annotations/sd-json.8f2a6a06dc3cf185f79d.js b/priv/static/static/js/emoji-annotations/sd-json.8f2a6a06dc3cf185f79d.js
new file mode 100644
index 000000000..a6a0f72e4
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sd-json.8f2a6a06dc3cf185f79d.js differ
diff --git a/priv/static/static/js/emoji-annotations/si-json.841b356da03623c10dbf.js b/priv/static/static/js/emoji-annotations/si-json.841b356da03623c10dbf.js
new file mode 100644
index 000000000..3344c5a55
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/si-json.841b356da03623c10dbf.js differ
diff --git a/priv/static/static/js/emoji-annotations/sk-json.5d96bdb4da82655d0314.js b/priv/static/static/js/emoji-annotations/sk-json.5d96bdb4da82655d0314.js
new file mode 100644
index 000000000..e7c9257bf
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sk-json.5d96bdb4da82655d0314.js differ
diff --git a/priv/static/static/js/emoji-annotations/sl-json.a40c5548da34fce6f1d0.js b/priv/static/static/js/emoji-annotations/sl-json.a40c5548da34fce6f1d0.js
new file mode 100644
index 000000000..e2a844e09
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sl-json.a40c5548da34fce6f1d0.js differ
diff --git a/priv/static/static/js/emoji-annotations/so-json.f75abd16637c8924c075.js b/priv/static/static/js/emoji-annotations/so-json.f75abd16637c8924c075.js
new file mode 100644
index 000000000..f30b4273c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/so-json.f75abd16637c8924c075.js differ
diff --git a/priv/static/static/js/emoji-annotations/sq-json.bcf154ed8a6138aa089c.js b/priv/static/static/js/emoji-annotations/sq-json.bcf154ed8a6138aa089c.js
new file mode 100644
index 000000000..be797564b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sq-json.bcf154ed8a6138aa089c.js differ
diff --git a/priv/static/static/js/emoji-annotations/sr-json.e29a20e59a708df0c6a8.js b/priv/static/static/js/emoji-annotations/sr-json.e29a20e59a708df0c6a8.js
new file mode 100644
index 000000000..78b6dbe97
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sr-json.e29a20e59a708df0c6a8.js differ
diff --git a/priv/static/static/js/emoji-annotations/sr_Cyrl_BA-json.83c7c64b9696ae2339f2.js b/priv/static/static/js/emoji-annotations/sr_Cyrl_BA-json.83c7c64b9696ae2339f2.js
new file mode 100644
index 000000000..80bf46826
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sr_Cyrl_BA-json.83c7c64b9696ae2339f2.js differ
diff --git a/priv/static/static/js/emoji-annotations/sr_Latn-json.ff27d1f455bf2afb8d70.js b/priv/static/static/js/emoji-annotations/sr_Latn-json.ff27d1f455bf2afb8d70.js
new file mode 100644
index 000000000..f3eaef6ba
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sr_Latn-json.ff27d1f455bf2afb8d70.js differ
diff --git a/priv/static/static/js/emoji-annotations/sr_Latn_BA-json.72933405032b81344754.js b/priv/static/static/js/emoji-annotations/sr_Latn_BA-json.72933405032b81344754.js
new file mode 100644
index 000000000..cef51c3b9
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sr_Latn_BA-json.72933405032b81344754.js differ
diff --git a/priv/static/static/js/emoji-annotations/su-json.daff15251020cbecea7d.js b/priv/static/static/js/emoji-annotations/su-json.daff15251020cbecea7d.js
new file mode 100644
index 000000000..6ed6b8c88
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/su-json.daff15251020cbecea7d.js differ
diff --git a/priv/static/static/js/emoji-annotations/sv-json.8b0374cbca3a77519876.js b/priv/static/static/js/emoji-annotations/sv-json.8b0374cbca3a77519876.js
new file mode 100644
index 000000000..f8ceb17a0
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sv-json.8b0374cbca3a77519876.js differ
diff --git a/priv/static/static/js/emoji-annotations/sw-json.d5f350641b9cbcc0e126.js b/priv/static/static/js/emoji-annotations/sw-json.d5f350641b9cbcc0e126.js
new file mode 100644
index 000000000..092c49415
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sw-json.d5f350641b9cbcc0e126.js differ
diff --git a/priv/static/static/js/emoji-annotations/sw_KE-json.f3563cbeac1c158563d5.js b/priv/static/static/js/emoji-annotations/sw_KE-json.f3563cbeac1c158563d5.js
new file mode 100644
index 000000000..6ddfc1169
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/sw_KE-json.f3563cbeac1c158563d5.js differ
diff --git a/priv/static/static/js/emoji-annotations/ta-json.675cc6c7607449d4a91b.js b/priv/static/static/js/emoji-annotations/ta-json.675cc6c7607449d4a91b.js
new file mode 100644
index 000000000..195764028
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ta-json.675cc6c7607449d4a91b.js differ
diff --git a/priv/static/static/js/emoji-annotations/te-json.8a69a10f62cdf626244e.js b/priv/static/static/js/emoji-annotations/te-json.8a69a10f62cdf626244e.js
new file mode 100644
index 000000000..a3a46e541
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/te-json.8a69a10f62cdf626244e.js differ
diff --git a/priv/static/static/js/emoji-annotations/tg-json.9647f559a4477d7d8e96.js b/priv/static/static/js/emoji-annotations/tg-json.9647f559a4477d7d8e96.js
new file mode 100644
index 000000000..0498c49a3
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/tg-json.9647f559a4477d7d8e96.js differ
diff --git a/priv/static/static/js/emoji-annotations/th-json.34709bae223bb9d2587c.js b/priv/static/static/js/emoji-annotations/th-json.34709bae223bb9d2587c.js
new file mode 100644
index 000000000..ba0bd0901
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/th-json.34709bae223bb9d2587c.js differ
diff --git a/priv/static/static/js/emoji-annotations/ti-json.5b173bd33bb960be805b.js b/priv/static/static/js/emoji-annotations/ti-json.5b173bd33bb960be805b.js
new file mode 100644
index 000000000..490406022
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ti-json.5b173bd33bb960be805b.js differ
diff --git a/priv/static/static/js/emoji-annotations/tk-json.23a9a6229829921704ad.js b/priv/static/static/js/emoji-annotations/tk-json.23a9a6229829921704ad.js
new file mode 100644
index 000000000..0e9d747eb
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/tk-json.23a9a6229829921704ad.js differ
diff --git a/priv/static/static/js/emoji-annotations/to-json.50ac37d101caaf592c94.js b/priv/static/static/js/emoji-annotations/to-json.50ac37d101caaf592c94.js
new file mode 100644
index 000000000..beb2f7492
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/to-json.50ac37d101caaf592c94.js differ
diff --git a/priv/static/static/js/emoji-annotations/tr-json.6462e8cfd5006cf5b6cf.js b/priv/static/static/js/emoji-annotations/tr-json.6462e8cfd5006cf5b6cf.js
new file mode 100644
index 000000000..41a40c8f8
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/tr-json.6462e8cfd5006cf5b6cf.js differ
diff --git a/priv/static/static/js/emoji-annotations/tt-json.4c089389ba9983ec8ef7.js b/priv/static/static/js/emoji-annotations/tt-json.4c089389ba9983ec8ef7.js
new file mode 100644
index 000000000..12322dd5a
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/tt-json.4c089389ba9983ec8ef7.js differ
diff --git a/priv/static/static/js/emoji-annotations/ug-json.bf768bd32e9ff02b0a8a.js b/priv/static/static/js/emoji-annotations/ug-json.bf768bd32e9ff02b0a8a.js
new file mode 100644
index 000000000..f03c7c634
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ug-json.bf768bd32e9ff02b0a8a.js differ
diff --git a/priv/static/static/js/emoji-annotations/uk-json.af110c8eef232638fc4d.js b/priv/static/static/js/emoji-annotations/uk-json.af110c8eef232638fc4d.js
new file mode 100644
index 000000000..92db43c2e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/uk-json.af110c8eef232638fc4d.js differ
diff --git a/priv/static/static/js/emoji-annotations/ur-json.983c02109444c883a18f.js b/priv/static/static/js/emoji-annotations/ur-json.983c02109444c883a18f.js
new file mode 100644
index 000000000..0a7755688
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/ur-json.983c02109444c883a18f.js differ
diff --git a/priv/static/static/js/emoji-annotations/uz-json.ac43f4c54d4587324a20.js b/priv/static/static/js/emoji-annotations/uz-json.ac43f4c54d4587324a20.js
new file mode 100644
index 000000000..d8113f17c
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/uz-json.ac43f4c54d4587324a20.js differ
diff --git a/priv/static/static/js/emoji-annotations/vi-json.f6a364b2add7f8c8bc67.js b/priv/static/static/js/emoji-annotations/vi-json.f6a364b2add7f8c8bc67.js
new file mode 100644
index 000000000..d444a6b83
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/vi-json.f6a364b2add7f8c8bc67.js differ
diff --git a/priv/static/static/js/emoji-annotations/wo-json.e0d689e22cda0dd77e9a.js b/priv/static/static/js/emoji-annotations/wo-json.e0d689e22cda0dd77e9a.js
new file mode 100644
index 000000000..469e8d68b
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/wo-json.e0d689e22cda0dd77e9a.js differ
diff --git a/priv/static/static/js/emoji-annotations/xh-json.21e88c05ad3113dfc7f2.js b/priv/static/static/js/emoji-annotations/xh-json.21e88c05ad3113dfc7f2.js
new file mode 100644
index 000000000..b535834ee
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/xh-json.21e88c05ad3113dfc7f2.js differ
diff --git a/priv/static/static/js/emoji-annotations/yo-json.ea1150d6bc360dd86f2e.js b/priv/static/static/js/emoji-annotations/yo-json.ea1150d6bc360dd86f2e.js
new file mode 100644
index 000000000..e9a628e79
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/yo-json.ea1150d6bc360dd86f2e.js differ
diff --git a/priv/static/static/js/emoji-annotations/yo_BJ-json.650318c25fe4da92bad8.js b/priv/static/static/js/emoji-annotations/yo_BJ-json.650318c25fe4da92bad8.js
new file mode 100644
index 000000000..05ef9d030
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/yo_BJ-json.650318c25fe4da92bad8.js differ
diff --git a/priv/static/static/js/emoji-annotations/yue-json.15102c0ddfdf19bdfb4c.js b/priv/static/static/js/emoji-annotations/yue-json.15102c0ddfdf19bdfb4c.js
new file mode 100644
index 000000000..37ff8c99e
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/yue-json.15102c0ddfdf19bdfb4c.js differ
diff --git a/priv/static/static/js/emoji-annotations/yue_Hans-json.cc60bb10d32fab3d8207.js b/priv/static/static/js/emoji-annotations/yue_Hans-json.cc60bb10d32fab3d8207.js
new file mode 100644
index 000000000..3ac6c6eb7
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/yue_Hans-json.cc60bb10d32fab3d8207.js differ
diff --git a/priv/static/static/js/emoji-annotations/zh-json.e42a28127d5c1aff6c85.js b/priv/static/static/js/emoji-annotations/zh-json.e42a28127d5c1aff6c85.js
new file mode 100644
index 000000000..aea5a2931
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/zh-json.e42a28127d5c1aff6c85.js differ
diff --git a/priv/static/static/js/emoji-annotations/zh_Hant-json.9cbb765c181d443828a9.js b/priv/static/static/js/emoji-annotations/zh_Hant-json.9cbb765c181d443828a9.js
new file mode 100644
index 000000000..859716678
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/zh_Hant-json.9cbb765c181d443828a9.js differ
diff --git a/priv/static/static/js/emoji-annotations/zh_Hant_HK-json.7eee03c705347a21c612.js b/priv/static/static/js/emoji-annotations/zh_Hant_HK-json.7eee03c705347a21c612.js
new file mode 100644
index 000000000..93360c7d6
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/zh_Hant_HK-json.7eee03c705347a21c612.js differ
diff --git a/priv/static/static/js/emoji-annotations/zu-json.e0cb7cd3890583fd0f6d.js b/priv/static/static/js/emoji-annotations/zu-json.e0cb7cd3890583fd0f6d.js
new file mode 100644
index 000000000..e68a4eb59
Binary files /dev/null and b/priv/static/static/js/emoji-annotations/zu-json.e0cb7cd3890583fd0f6d.js differ
diff --git a/priv/static/static/js/emoji.33eab91b64f59431137d.js b/priv/static/static/js/emoji.33eab91b64f59431137d.js
new file mode 100644
index 000000000..16871f9fd
Binary files /dev/null and b/priv/static/static/js/emoji.33eab91b64f59431137d.js differ
diff --git a/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js b/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js
new file mode 100644
index 000000000..d824f3dcd
Binary files /dev/null and b/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js differ
diff --git a/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js.map b/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js.map
new file mode 100644
index 000000000..65712172a
Binary files /dev/null and b/priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js.map differ
diff --git a/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js b/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js
new file mode 100644
index 000000000..2cb375ebb
Binary files /dev/null and b/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js differ
diff --git a/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js.map b/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js.map
new file mode 100644
index 000000000..5bcbe147d
Binary files /dev/null and b/priv/static/static/js/i18n/ca-json.1eb24bc001efa3c0627f.js.map differ
diff --git a/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js b/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js
new file mode 100644
index 000000000..f75b1386f
Binary files /dev/null and b/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js differ
diff --git a/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js.map b/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js.map
new file mode 100644
index 000000000..03551325d
Binary files /dev/null and b/priv/static/static/js/i18n/cs-json.5eedbe9f7084c349fbe8.js.map differ
diff --git a/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js b/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js
new file mode 100644
index 000000000..a796f7965
Binary files /dev/null and b/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js differ
diff --git a/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js.map b/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js.map
new file mode 100644
index 000000000..fffc2b548
Binary files /dev/null and b/priv/static/static/js/i18n/de-json.bfa48615ac92f87ff37a.js.map differ
diff --git a/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js b/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js
new file mode 100644
index 000000000..faf8906d1
Binary files /dev/null and b/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js differ
diff --git a/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js.map b/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js.map
new file mode 100644
index 000000000..729878820
Binary files /dev/null and b/priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js.map differ
diff --git a/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js b/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js
new file mode 100644
index 000000000..47487f099
Binary files /dev/null and b/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js differ
diff --git a/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js.map b/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js.map
new file mode 100644
index 000000000..c0b5048ec
Binary files /dev/null and b/priv/static/static/js/i18n/es-json.4a302899a65e1f67d8a2.js.map differ
diff --git a/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js b/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js
new file mode 100644
index 000000000..d6983a24c
Binary files /dev/null and b/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js differ
diff --git a/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js.map b/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js.map
new file mode 100644
index 000000000..3a37ba0fc
Binary files /dev/null and b/priv/static/static/js/i18n/et-json.580b101d6bb83a8aee75.js.map differ
diff --git a/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js b/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js
new file mode 100644
index 000000000..60bb8f308
Binary files /dev/null and b/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js differ
diff --git a/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js.map b/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js.map
new file mode 100644
index 000000000..ed514484b
Binary files /dev/null and b/priv/static/static/js/i18n/eu-json.d2eab39b0427995fc314.js.map differ
diff --git a/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js b/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js
new file mode 100644
index 000000000..86fc97a69
Binary files /dev/null and b/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js differ
diff --git a/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js.map b/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js.map
new file mode 100644
index 000000000..e4e261aef
Binary files /dev/null and b/priv/static/static/js/i18n/fa-json.a19100f6a5a9431e2adb.js.map differ
diff --git a/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js b/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js
new file mode 100644
index 000000000..6cdb0ca16
Binary files /dev/null and b/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js differ
diff --git a/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js.map b/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js.map
new file mode 100644
index 000000000..eb936fb93
Binary files /dev/null and b/priv/static/static/js/i18n/fi-json.d1934c18f12d80493ab1.js.map differ
diff --git a/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js b/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js
new file mode 100644
index 000000000..cb0b7ee37
Binary files /dev/null and b/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js differ
diff --git a/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js.map b/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js.map
new file mode 100644
index 000000000..bcfbf36c7
Binary files /dev/null and b/priv/static/static/js/i18n/fr-json.36ef21ccb1203d3e65ae.js.map differ
diff --git a/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js b/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js
new file mode 100644
index 000000000..fed2747ad
Binary files /dev/null and b/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js differ
diff --git a/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js.map b/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js.map
new file mode 100644
index 000000000..f08827ead
Binary files /dev/null and b/priv/static/static/js/i18n/ga-json.3f1981817977a16f1d7e.js.map differ
diff --git a/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js b/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js
new file mode 100644
index 000000000..1b9aa9e67
Binary files /dev/null and b/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js differ
diff --git a/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js.map b/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js.map
new file mode 100644
index 000000000..65f58a78f
Binary files /dev/null and b/priv/static/static/js/i18n/he-json.03c5d271a4034de06e6f.js.map differ
diff --git a/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js b/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js
new file mode 100644
index 000000000..207109ad4
Binary files /dev/null and b/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js differ
diff --git a/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js.map b/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js.map
new file mode 100644
index 000000000..4ed6b382a
Binary files /dev/null and b/priv/static/static/js/i18n/hu-json.0487899ca7a7a8505ed8.js.map differ
diff --git a/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js b/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js
new file mode 100644
index 000000000..19fd2c981
Binary files /dev/null and b/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js differ
diff --git a/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js.map b/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js.map
new file mode 100644
index 000000000..8aae96495
Binary files /dev/null and b/priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js.map differ
diff --git a/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js b/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js
new file mode 100644
index 000000000..26774f82f
Binary files /dev/null and b/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js differ
diff --git a/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js.map b/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js.map
new file mode 100644
index 000000000..0081d7d5a
Binary files /dev/null and b/priv/static/static/js/i18n/it-json.99a21d5c98376af17141.js.map differ
diff --git a/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js b/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js
new file mode 100644
index 000000000..1c2d18099
Binary files /dev/null and b/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js differ
diff --git a/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js.map b/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js.map
new file mode 100644
index 000000000..d2ba609ba
Binary files /dev/null and b/priv/static/static/js/i18n/ja_easy-json.1d5ea7e755b066ac2cdd.js.map differ
diff --git a/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js b/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js
new file mode 100644
index 000000000..d099401b0
Binary files /dev/null and b/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js differ
diff --git a/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js.map b/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js.map
new file mode 100644
index 000000000..65fbc1d26
Binary files /dev/null and b/priv/static/static/js/i18n/ja_pedantic-json.b52fa70f0bf89ae01cfb.js.map differ
diff --git a/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js b/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js
new file mode 100644
index 000000000..0fa397271
Binary files /dev/null and b/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js differ
diff --git a/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js.map b/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js.map
new file mode 100644
index 000000000..3d37b94b4
Binary files /dev/null and b/priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js.map differ
diff --git a/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js b/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js
new file mode 100644
index 000000000..658a3e71f
Binary files /dev/null and b/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js differ
diff --git a/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js.map b/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js.map
new file mode 100644
index 000000000..fa8649aa7
Binary files /dev/null and b/priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js.map differ
diff --git a/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js b/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js
new file mode 100644
index 000000000..757736ff9
Binary files /dev/null and b/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js differ
diff --git a/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js.map b/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js.map
new file mode 100644
index 000000000..5da264f3d
Binary files /dev/null and b/priv/static/static/js/i18n/nb-json.a54af3b1f47d576ad4aa.js.map differ
diff --git a/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js b/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js
new file mode 100644
index 000000000..22cf4c67f
Binary files /dev/null and b/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js differ
diff --git a/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js.map b/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js.map
new file mode 100644
index 000000000..66f972d58
Binary files /dev/null and b/priv/static/static/js/i18n/nl-json.3fb9758b10c29434b613.js.map differ
diff --git a/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js b/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js
new file mode 100644
index 000000000..8fc37e9fa
Binary files /dev/null and b/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js differ
diff --git a/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js.map b/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js.map
new file mode 100644
index 000000000..6ccb46676
Binary files /dev/null and b/priv/static/static/js/i18n/oc-json.4f52bf1b6e3213acc33c.js.map differ
diff --git a/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js b/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js
new file mode 100644
index 000000000..b85acddb7
Binary files /dev/null and b/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js differ
diff --git a/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js.map b/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js.map
new file mode 100644
index 000000000..160ccd4e9
Binary files /dev/null and b/priv/static/static/js/i18n/pl-json.c963247822381b05579b.js.map differ
diff --git a/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js b/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js
new file mode 100644
index 000000000..cf7039fe6
Binary files /dev/null and b/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js differ
diff --git a/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js.map b/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js.map
new file mode 100644
index 000000000..afd81f273
Binary files /dev/null and b/priv/static/static/js/i18n/pt-json.3fc5593e030268bcd291.js.map differ
diff --git a/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js b/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js
new file mode 100644
index 000000000..74cbf5e0a
Binary files /dev/null and b/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js differ
diff --git a/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js.map b/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js.map
new file mode 100644
index 000000000..dd62acce7
Binary files /dev/null and b/priv/static/static/js/i18n/ro-json.3fd9977ed1c1413059ae.js.map differ
diff --git a/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js b/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js
new file mode 100644
index 000000000..d1b02c3c8
Binary files /dev/null and b/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js differ
diff --git a/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js.map b/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js.map
new file mode 100644
index 000000000..2832cf543
Binary files /dev/null and b/priv/static/static/js/i18n/ru-json.b913eb7f7e9f0c642438.js.map differ
diff --git a/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js b/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js
new file mode 100644
index 000000000..e69fdbdf4
Binary files /dev/null and b/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js differ
diff --git a/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js.map b/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js.map
new file mode 100644
index 000000000..b891e85d0
Binary files /dev/null and b/priv/static/static/js/i18n/sk-json.d8d0eba80f94e6f55145.js.map differ
diff --git a/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js b/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js
new file mode 100644
index 000000000..dd88b1d53
Binary files /dev/null and b/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js differ
diff --git a/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js.map b/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js.map
new file mode 100644
index 000000000..2a67a2635
Binary files /dev/null and b/priv/static/static/js/i18n/te-json.ce6db28261b2f824064f.js.map differ
diff --git a/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js b/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js
new file mode 100644
index 000000000..54a7b0409
Binary files /dev/null and b/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js differ
diff --git a/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js.map b/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js.map
new file mode 100644
index 000000000..0bc713664
Binary files /dev/null and b/priv/static/static/js/i18n/uk-json.003908af9b15becdd382.js.map differ
diff --git a/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js b/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js
new file mode 100644
index 000000000..f942a6c7f
Binary files /dev/null and b/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js differ
diff --git a/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js.map b/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js.map
new file mode 100644
index 000000000..cee2126af
Binary files /dev/null and b/priv/static/static/js/i18n/vi-json.703c1e731be6e857bbf3.js.map differ
diff --git a/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js b/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js
new file mode 100644
index 000000000..6e2fbf61f
Binary files /dev/null and b/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js differ
diff --git a/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js.map b/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js.map
new file mode 100644
index 000000000..a844ccca1
Binary files /dev/null and b/priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js.map differ
diff --git a/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js b/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js
new file mode 100644
index 000000000..00f6850b5
Binary files /dev/null and b/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js differ
diff --git a/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js.map b/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js.map
new file mode 100644
index 000000000..b8ca3f415
Binary files /dev/null and b/priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js.map differ
diff --git a/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js b/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js
deleted file mode 100644
index 5ffbf5a2b..000000000
Binary files a/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js.map b/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js.map
deleted file mode 100644
index cd09905ec..000000000
Binary files a/priv/static/static/js/vendors~app.cea10ab53f3aa19fc30e.js.map and /dev/null differ
diff --git a/priv/static/static/ruffle/56f009143a5a7685fad9.wasm b/priv/static/static/ruffle/56f009143a5a7685fad9.wasm
new file mode 100644
index 000000000..00ce8421e
Binary files /dev/null and b/priv/static/static/ruffle/56f009143a5a7685fad9.wasm differ
diff --git a/priv/static/static/ruffle/92614a5efc3434baeaa9.wasm b/priv/static/static/ruffle/92614a5efc3434baeaa9.wasm
new file mode 100644
index 000000000..5cf135b6a
Binary files /dev/null and b/priv/static/static/ruffle/92614a5efc3434baeaa9.wasm differ
diff --git a/priv/static/static/ruffle/LICENSE_MIT b/priv/static/static/ruffle/LICENSE_MIT
index 63a286b4f..941fe9938 100644
--- a/priv/static/static/ruffle/LICENSE_MIT
+++ b/priv/static/static/ruffle/LICENSE_MIT
@@ -1,4 +1,4 @@
-Copyright (c) 2018 Mike Welsh
+Copyright (c) 2018 Ruffle LLC and Ruffle contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
diff --git a/priv/static/static/ruffle/README.md b/priv/static/static/ruffle/README.md
index 25636e78f..c59b8c9b0 100644
--- a/priv/static/static/ruffle/README.md
+++ b/priv/static/static/ruffle/README.md
@@ -18,7 +18,7 @@ to get up and running. If you'd prefer to use Ruffle through npm and a bundler,
Before you can get started with using Ruffle on your website, you must host its files yourself.
Either take the [latest build](https://github.com/ruffle-rs/ruffle/releases)
-or [build it yourself](../../README.md), and make these files accessible by your web server.
+or [build it yourself](https://github.com/ruffle-rs/ruffle/blob/master/web/README.md), and make these files accessible by your web server.
Please note that the `.wasm` file must be served properly, and some web servers may not do that
correctly out of the box. Please see [our wiki](https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-wasm-mime-type)
@@ -54,4 +54,4 @@ If you want to control the Ruffle player, you may use our Javascript API.
## Building, testing or contributing
-Please see [the ruffle-web README](../../README.md).
+Please see [the ruffle-web README](https://github.com/ruffle-rs/ruffle/blob/master/web/README.md).
diff --git a/priv/static/static/ruffle/af9b9e80cef829d41f6454bfef68d005.wasm b/priv/static/static/ruffle/af9b9e80cef829d41f6454bfef68d005.wasm
deleted file mode 100644
index 1d1a00d12..000000000
Binary files a/priv/static/static/ruffle/af9b9e80cef829d41f6454bfef68d005.wasm and /dev/null differ
diff --git a/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js b/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js
new file mode 100644
index 000000000..482fefe92
Binary files /dev/null and b/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js differ
diff --git a/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js.map b/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js.map
new file mode 100644
index 000000000..0c97a3b1f
Binary files /dev/null and b/priv/static/static/ruffle/core.ruffle.61b3dd915983ae8a8b16.js.map differ
diff --git a/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js b/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js
new file mode 100644
index 000000000..1b2e68f88
Binary files /dev/null and b/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js differ
diff --git a/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js.map b/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js.map
new file mode 100644
index 000000000..70f8ce17c
Binary files /dev/null and b/priv/static/static/ruffle/core.ruffle.848d766d6fc336164c2f.js.map differ
diff --git a/priv/static/static/ruffle/package.json b/priv/static/static/ruffle/package.json
index 6f3cbfbe2..032e39f84 100644
--- a/priv/static/static/ruffle/package.json
+++ b/priv/static/static/ruffle/package.json
@@ -1 +1 @@
-{"name": "ruffle-mirror", "version": "2021.4.11", "description": "This is an auto npm mirror for ruffle nightly builds.", "repository": {"type": "git", "url": "git+https://github.com/rwv/ruffle-mirror.git"}, "author": "ruffle-rs", "license": "MIT", "bugs": {"url": "https://github.com/rwv/ruffle-mirror/issues"}, "homepage": "https://github.com/rwv/ruffle-mirror#readme"}
\ No newline at end of file
+{"name":"@ruffle-rs/ruffle","version":"0.1.0-nightly.2022.07.12","description":"Putting Flash back on the web. Ruffle will polyfill all Flash content and replace it with the Ruffle flash player.","license":"(MIT OR Apache-2.0)","keywords":["flash","swf"],"homepage":"https://ruffle.rs","bugs":"https://github.com/ruffle-rs/ruffle/issues","repository":"github:ruffle-rs/ruffle","main":"ruffle.js"}
\ No newline at end of file
diff --git a/priv/static/static/ruffle/ruffle.js b/priv/static/static/ruffle/ruffle.js
index d4c5a5dd9..fca1dfa7c 100644
Binary files a/priv/static/static/ruffle/ruffle.js and b/priv/static/static/ruffle/ruffle.js differ
diff --git a/priv/static/static/ruffle/ruffle.js.map b/priv/static/static/ruffle/ruffle.js.map
index dcbb7add8..725bbfec7 100644
Binary files a/priv/static/static/ruffle/ruffle.js.map and b/priv/static/static/ruffle/ruffle.js.map differ
diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js
index b1699a5a9..d3273e6e4 100644
Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ
diff --git a/priv/static/sw-pleroma.js.LICENSE.txt b/priv/static/sw-pleroma.js.LICENSE.txt
new file mode 100644
index 000000000..63c4ca54c
--- /dev/null
+++ b/priv/static/sw-pleroma.js.LICENSE.txt
@@ -0,0 +1,28 @@
+/*!
+ localForage -- Offline Storage, Improved
+ Version 1.10.0
+ https://localforage.github.io/localForage
+ (c) 2013-2017 Mozilla, Apache License 2.0
+*/
+
+/*!
+ * devtools-if v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */
+
+/*!
+ * shared v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */
+
+/*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */
+
+/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map
index 06813ad0e..8170d0ecc 100644
Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ
diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/pleroma
index dea1db26c..ca5b842e1 100755
--- a/rel/files/installation/init.d/pleroma
+++ b/rel/files/installation/init.d/pleroma
@@ -9,6 +9,7 @@ command=/opt/pleroma/bin/pleroma
command_args="start"
command_user=pleroma
command_background=1
+no_new_privs="yes"
# Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30/SIGKILL/5"
diff --git a/test/fixtures/custom-emoji-reaction.json b/test/fixtures/custom-emoji-reaction.json
new file mode 100644
index 000000000..003de0511
--- /dev/null
+++ b/test/fixtures/custom-emoji-reaction.json
@@ -0,0 +1,28 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "Hashtag": "as:Hashtag"
+ }
+ ],
+ "type": "Like",
+ "id": "https://misskey.local.live/likes/917ocsybgp",
+ "actor": "https://misskey.local.live/users/8x8yep20u2",
+ "object": "https://pleroma.local.live/objects/89937a53-2692-4631-bb62-770091267391",
+ "content": ":hanapog:",
+ "_misskey_reaction": ":hanapog:",
+ "tag": [
+ {
+ "id": "https://misskey.local.live/emojis/hanapog",
+ "type": "Emoji",
+ "name": ":hanapog:",
+ "updated": "2022-06-07T12:00:05.773Z",
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/fep-e232.json b/test/fixtures/fep-e232.json
new file mode 100644
index 000000000..e9d12ae35
--- /dev/null
+++ b/test/fixtures/fep-e232.json
@@ -0,0 +1,31 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "type": "Create",
+ "actor": "https://example.org/users/alice",
+ "object": {
+ "id": "https://example.org/objects/10",
+ "type": "Note",
+ "attributedTo": "https://example.org/users/alice",
+ "content": "test https://example.org/objects/9
",
+ "published": "2022-10-01T21:30:05.211215Z",
+ "tag": [
+ {
+ "name": "@bob@example.net",
+ "type": "Mention",
+ "href": "https://example.net/users/bob"
+ },
+ {
+ "name": "https://example.org/objects/9",
+ "type": "Link",
+ "href": "https://example.org/objects/9",
+ "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+ }
+ ],
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://example.org/users/alice/followers"
+ ]
+ }
+}
diff --git a/test/fixtures/hubzilla-actor.json b/test/fixtures/hubzilla-actor.json
new file mode 100644
index 000000000..445d6413c
--- /dev/null
+++ b/test/fixtures/hubzilla-actor.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hub.somaton.com/apschema/v1.9"],"type":"Person","id":"https://hub.somaton.com/channel/testc6","preferredUsername":"testc6","name":"testc6 lala","updated":"2021-08-29T10:07:23Z","icon":{"type":"Image","mediaType":"image/png","updated":"2021-10-09T04:54:35Z","url":"https://hub.somaton.com/photo/profile/l/33","height":300,"width":300},"url":"https://hub.somaton.com/channel/testc6","publicKey":{"id":"https://hub.somaton.com/channel/testc6","owner":"https://hub.somaton.com/channel/testc6","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq5ep+6MhhaAiqZSd8nXe\nUAokXNgqTr/DjUic5VDudjQgvetchaiBUieBnqpJSPNNAvvf6Qs4eDW4w2JQeA6y\nqEplKrmb8l1EyhwXeFLDUGQdf0f6hg++x5mIrO6uX0tlQGU6nutvhItn6JMZc5GU\nv3C/UW0OfHCCdHSGZ/1nIqq1P98FqF0+PA1pvTHCkLr4kcKzfpmkLjsccUSq0FGh\nQF+paW9FU89o4hkaH/X3E/Ac7DL8zgcyt29KSj4eUIvjBIEPAMdRno345fiZ+QYr\nlYQYaBC2gvozjxtxl9MyfqjBRzfl9VDHzoDvMn5+LD5dCRB1zOESv/b3EpiHYqXl\nwiPzP9az8e8cw6D72n/Mlrf27yIuVAdwaGdbAwekjIQZHIDoP0XNnA5i31RLpEMI\nbNpH47ChtjxeilQZ3va6qIShYfGlndpy/rx4i4Yt4xIG+BbGb/dWo3AbtHi64fPZ\nMoLuR71sEBe7uAvalJ+lopxuQ2qLJpCInukQ13p/G/n9tVDwbfGyumzr5hHk7JoY\nN+JqH737MCZqb9dRDof+fju58GY1VzFjBph38sHYJh0ykA+2BzYU2+nT7CDXfKWA\nsmHhizp7haoPjl/yclZG5FJwg3oqHTD14dASUs+OI4K+Q//74wfb4/6E3CDyOkW3\nUj+8TPZooKulxtQ9ezergr0CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"outbox":"https://hub.somaton.com/outbox/testc6","inbox":"https://hub.somaton.com/inbox/testc6","followers":"https://hub.somaton.com/followers/testc6","following":"https://hub.somaton.com/following/testc6","endpoints":{"sharedInbox":"https://hub.somaton.com/inbox"},"discoverable":false,"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"8d6dea03f04cbb7faaf43958a4cf39a115ff1c61c7febaa6154c463eab9a42c8","creator":"https://hub.somaton.com/channel/testc6","created":"2021-10-13T18:21:48Z","signatureValue":"N4CJBO2K/8v7KI97REyJXaSYOlLWscuEDlODDnjNYD1fbVQFO3s2JtqPcN2lVJvNTlW5HUze+owaAYNcvZe3mNm1iz05Xru3s8yRA8bNCdKBuWd/3zb3/JQVkbSb09D2PloeuoKBQmPIn+dNiTyFR0jxLsxCXXTomGKigWPtTOUIt52Dv9MFJ3jRZmfoykT9bHrAIVCASHoiluhTkPAzc6pt0lSyZd0D3X4J1K4/sLXa8HRoooMFu2dHWfqV4tyLU9WzofAhvnYg9tEbKCH42DIAbwDfjAeC4qL8xkqAlYWLvXYVGH76cZLdp9Zuv1p3NHqaPEJ85MbuaUkfnU75Bx/Fcfoi0pEieWRdFvMx5b/UFwGbJd6iSAO1zRbGYTPEMPWHzh0AEAaLeyY+g3ZmpNu88ujrIr8iJ1U4EkjOBn8ooxA5LaI2fXDiYC2NwRiAbY+xVtgJgvHDi9tXCdvzjZWfU/cgiwF/cYMbsB2BCyPRd+XZhudfXSOysFC4WYnawhiRVevba9lQ6rEP4FMepOGq4ZOSGzxgw2xNIXpu0IkrxX5mEv/ahEhDy1KGRIFc0GnPJrv3kMVxJrZ7SF8PNAGqftQBLkqQR+SEygs3XB4cd2DQ2lPeiMd8+Xv+lBjtzZtZAM/Y4CZCOdV9DHXDGNSKKFDzzna4QcUzQ+KRc8w="}}
\ No newline at end of file
diff --git a/test/fixtures/hubzilla-create-image.json b/test/fixtures/hubzilla-create-image.json
new file mode 100644
index 000000000..9f0669bb7
--- /dev/null
+++ b/test/fixtures/hubzilla-create-image.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hub.somaton.com/apschema/v1.9"],"type":"Create","id":"https://hub.somaton.com/activity/452583b2-7e1f-4ac3-8334-ff666f134afe","diaspora:guid":"452583b2-7e1f-4ac3-8334-ff666f134afe","name":"daf82c18ef92a84cda72(1).jpg","published":"2021-10-12T21:28:26Z","actor":"https://hub.somaton.com/channel/testc6","object":{"type":"Image","name":"daf82c18ef92a84cda72(1).jpg","published":"2021-10-12T21:28:23Z","updated":"2021-10-12T21:28:23Z","attributedTo":"https://hub.somaton.com/channel/testc6","id":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe","url":[{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-0.jpg","width":2200,"height":2200},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-1.jpg","width":1024,"height":1024},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-2.jpg","width":640,"height":640},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-3.jpg","width":320,"height":320},{"type":"Link","mediaType":"text/html","href":"https://hub.somaton.com/photos/testc6/image/452583b2-7e1f-4ac3-8334-ff666f134afe"}],"source":{"content":"[footer][zrl=https://hub.somaton.com/channel/testc6]testc6 lala[/zrl] posted [zrl=https://hub.somaton.com/photos/testc6/image/452583b2-7e1f-4ac3-8334-ff666f134afe]a new photo[/zrl] to [zrl=https://hub.somaton.com/photos/testc6/album/1e9b0d74-633e-4bd0-b37f-694bb0ed0145]test[/zrl][/footer]","mediaType":"text/bbcode"},"content":"","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hub.somaton.com/followers/testc6"]},"target":{"type":"orderedCollection","name":"test","id":"https://hub.somaton.com/album/testc6/test"},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hub.somaton.com/followers/testc6"],"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"e0d077edccf262f02ed59ff67e91a5324ccaffc3d2b3f23793b4bd24cdbe70bb","creator":"https://hub.somaton.com/channel/testc6","created":"2021-10-13T18:39:05Z","signatureValue":"YYU0/17PqqUmLCn4oVS2N62rV1G9WQ+wLax2cI+EpMw/WOWKuVvtGrvhzciQ5ITXoh3scrZRYH8Bke1jDWkjL9YtjVD6TjMsv6f3OoO1vvMNgEfQfgZJ78QQt5MoLrT2mkRa35lSmVHkTDROKJPrwIAnpN6bDb577wZ63BsuBjqW7ca/E6oXSIr+meCXv3kqkyYDSz0ImYvVmki+OfX97xbYkQlzM06EgK1LZTHfuf4sk09hVfDDqVB9tHO4ObYQCYNiOWRHjA5S1Cw8WX1OQJ+GCQ8yxHmtiU3tJsxeYhxGs7VEmTLUvf/QZ0VTPumkd1CewdxzNGvAP3f9JCakuV7eyk88oqF+p7xxfxmBjLYbMTuhrcZIdUdMcjW9pENOYBbt+a+FhPsjbm8zVU3iKPqe/8UAvo01hGW7jiKJUm4qdcX3H3MExTLEFuz0NTeqxl4djlyGTT9KBqNouD+/oSSgwm6qeRZ5y3RsC27N0HRbg74qNXhhWQZVWQtHdSCHjAfHVPOSpjxpSPs7qkMLQ0vPsVsCsukZz8JCoXRo+JoKuaiaRgfiIRGNBO/XEicKMyu2JCU+UmkroiDJHy+4IfZRevnlneRa1jmu5KA/4xk5KU8l0I0Inap7TSPhv14Ex2sF89LkT8MbcDM3S3QL4urYsQj37zOKRDTFzE96TmI="}}
\ No newline at end of file
diff --git a/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png b/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png
new file mode 100644
index 000000000..7ce8640fa
Binary files /dev/null and b/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png differ
diff --git a/test/fixtures/image_with_stray_data_after.png b/test/fixtures/image_with_stray_data_after.png
new file mode 100755
index 000000000..a280e4377
Binary files /dev/null and b/test/fixtures/image_with_stray_data_after.png differ
diff --git a/test/fixtures/mastodon-nodeinfo20.json b/test/fixtures/mastodon-nodeinfo20.json
new file mode 100644
index 000000000..35010fdf0
--- /dev/null
+++ b/test/fixtures/mastodon-nodeinfo20.json
@@ -0,0 +1 @@
+{"version":"2.0","software":{"name":"mastodon","version":"4.1.0"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":971090,"activeMonth":167218,"activeHalfyear":384808},"localPosts":52071541},"openRegistrations":true,"metadata":{}}
\ No newline at end of file
diff --git a/test/fixtures/mastodon-well-known-nodeinfo.json b/test/fixtures/mastodon-well-known-nodeinfo.json
new file mode 100644
index 000000000..237d5462a
--- /dev/null
+++ b/test/fixtures/mastodon-well-known-nodeinfo.json
@@ -0,0 +1 @@
+{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://mastodon.example.org/nodeinfo/2.0"}]}
\ No newline at end of file
diff --git a/test/fixtures/quote_post/fedibird_quote_mismatched.json b/test/fixtures/quote_post/fedibird_quote_mismatched.json
new file mode 100644
index 000000000..8dee5daff
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_mismatched.json
@@ -0,0 +1,54 @@
+{
+ "@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",
+ "fedibird": "http://fedibird.com/ns#",
+ "quoteUri": "fedibird:quoteUri",
+ "expiry": "fedibird:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-30T15:44:50Z",
+ "url": "https://fedibird.com/@noellabo/107712183700212249",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107712183700212249",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-30:objectId=107712183700170473:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107712183700170473",
+ "quoteUri": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434",
+ "_misskey_quote": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434",
+ "_misskey_content": "揺れていたようだ",
+ "content": "揺れていたようだ QT: https:// unnerv.jp/@UN_NERV/10771217684 9067434
",
+ "contentMap": {
+ "ja": "揺れていたようだ QT: https:// unnerv.jp/@UN_NERV/10771217684 9067434
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/quote_post/fedibird_quote_post.json b/test/fixtures/quote_post/fedibird_quote_post.json
new file mode 100644
index 000000000..ebf383356
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_post.json
@@ -0,0 +1,52 @@
+{
+ "@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",
+ "expiry": "toot:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-22T02:07:16Z",
+ "url": "https://fedibird.com/@noellabo/107663670404015196",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107663670404015196",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-22:objectId=107663670404038002:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107663670404038002",
+ "quoteURL": "https://misskey.io/notes/8vsn2izjwh",
+ "_misskey_quote": "https://misskey.io/notes/8vsn2izjwh",
+ "_misskey_content": "いつの生まれだシトリン",
+ "content": "いつの生まれだシトリン QT: https:// misskey.io/notes/8vsn2izjwh
",
+ "contentMap": {
+ "ja": "いつの生まれだシトリン QT: https:// misskey.io/notes/8vsn2izjwh
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/quote_post/fedibird_quote_uri.json b/test/fixtures/quote_post/fedibird_quote_uri.json
new file mode 100644
index 000000000..7c328fdb9
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_uri.json
@@ -0,0 +1,54 @@
+{
+ "@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",
+ "fedibird": "http://fedibird.com/ns#",
+ "quoteUri": "fedibird:quoteUri",
+ "expiry": "fedibird:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-28T09:17:30Z",
+ "url": "https://fedibird.com/@noellabo/107699335988346142",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107699335988346142",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-28:objectId=107699335988345290:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107699335988345290",
+ "quoteUri": "https://fedibird.com/users/yamako/statuses/107699333438289729",
+ "_misskey_quote": "https://fedibird.com/users/yamako/statuses/107699333438289729",
+ "_misskey_content": "美味しそう",
+ "content": "美味しそう QT: https:// fedibird.com/@yamako/107699333 438289729
",
+ "contentMap": {
+ "ja": "美味しそう QT: https:// fedibird.com/@yamako/107699333 438289729
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/quote_post/fep-e232-tag-example.json b/test/fixtures/quote_post/fep-e232-tag-example.json
new file mode 100644
index 000000000..23c7fb5ac
--- /dev/null
+++ b/test/fixtures/quote_post/fep-e232-tag-example.json
@@ -0,0 +1,17 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "type": "Note",
+ "content": "This is a quote: RE: https://server.example/objects/123",
+ "tag": [
+ {
+ "type": "Link",
+ "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ "href": "https://server.example/objects/123",
+ "name": "RE: https://server.example/objects/123"
+ }
+ ],
+ "id": "https://server.example/objects/1",
+ "to": "https://server.example/users/1",
+ "attributedTo": "https://server.example/users/1",
+ "actor": "https://server.example/users/1"
+}
diff --git a/test/fixtures/quote_post/misskey_quote_post.json b/test/fixtures/quote_post/misskey_quote_post.json
new file mode 100644
index 000000000..59f677ca9
--- /dev/null
+++ b/test/fixtures/quote_post/misskey_quote_post.json
@@ -0,0 +1,46 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "id": "https://misskey.io/notes/8vs6ylpfez",
+ "type": "Note",
+ "attributedTo": "https://misskey.io/users/7rkrarq81i",
+ "summary": null,
+ "content": "投稿者の設定によるね Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある RE: https://misskey.io/notes/8vs6wxufd0
",
+ "_misskey_content": "投稿者の設定によるね\nFanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある",
+ "_misskey_quote": "https://misskey.io/notes/8vs6wxufd0",
+ "quoteUrl": "https://misskey.io/notes/8vs6wxufd0",
+ "published": "2022-01-21T16:38:30.243Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://misskey.io/users/7rkrarq81i/followers"
+ ],
+ "inReplyTo": null,
+ "attachment": [],
+ "sensitive": false,
+ "tag": []
+}
diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json
new file mode 100644
index 000000000..9ff4cb6d0
--- /dev/null
+++ b/test/fixtures/tesla_mock/aimu@misskey.io.json
@@ -0,0 +1,64 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "type": "Person",
+ "id": "https://misskey.io/users/83ssedkv53",
+ "inbox": "https://misskey.io/users/83ssedkv53/inbox",
+ "outbox": "https://misskey.io/users/83ssedkv53/outbox",
+ "followers": "https://misskey.io/users/83ssedkv53/followers",
+ "following": "https://misskey.io/users/83ssedkv53/following",
+ "sharedInbox": "https://misskey.io/inbox",
+ "endpoints": {
+ "sharedInbox": "https://misskey.io/inbox"
+ },
+ "url": "https://misskey.io/@aimu",
+ "preferredUsername": "aimu",
+ "name": "あいむ",
+ "summary": "わずかな作曲要素 巣穴で独り言 Twitter https://twitter.com/aimu_53 Soundcloud https://soundcloud.com/aimu-53
",
+ "icon": {
+ "type": "Image",
+ "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg",
+ "sensitive": false,
+ "name": null
+ },
+ "image": {
+ "type": "Image",
+ "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png",
+ "sensitive": false,
+ "name": null
+ },
+ "tag": [],
+ "manuallyApprovesFollowers": false,
+ "discoverable": true,
+ "publicKey": {
+ "id": "https://misskey.io/users/83ssedkv53#main-key",
+ "type": "Key",
+ "owner": "https://misskey.io/users/83ssedkv53",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n"
+ },
+ "isCat": true,
+ "vcard:bday": "5353-05-03"
+}
diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json
new file mode 100644
index 000000000..323ca10ed
--- /dev/null
+++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json
@@ -0,0 +1,44 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "id": "https://misskey.io/notes/8vs6wxufd0",
+ "type": "Note",
+ "attributedTo": "https://misskey.io/users/83ssedkv53",
+ "summary": null,
+ "content": "Fantiaこれできないように過去のやつは従量課金だった気がする
",
+ "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする",
+ "published": "2022-01-21T16:37:12.663Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://misskey.io/users/83ssedkv53/followers"
+ ],
+ "inReplyTo": null,
+ "attachment": [],
+ "sensitive": false,
+ "tag": []
+}
diff --git a/test/fixtures/wildebeest-nodeinfo21.json b/test/fixtures/wildebeest-nodeinfo21.json
new file mode 100644
index 000000000..c6af474bf
--- /dev/null
+++ b/test/fixtures/wildebeest-nodeinfo21.json
@@ -0,0 +1 @@
+{"version":"2.1","software":{"name":"wildebeest","version":"0.0.1","repository":"https://github.com/cloudflare/wildebeest"},"protocols":["activitypub"],"usage":{"users":{"total":1,"activeMonth":1,"activeHalfyear":1}},"openRegistrations":false,"metadata":{"upstream":{"name":"mastodon","version":"3.5.1"}}}
\ No newline at end of file
diff --git a/test/fixtures/wildebeest-well-known-nodeinfo.json b/test/fixtures/wildebeest-well-known-nodeinfo.json
new file mode 100644
index 000000000..c7ddb43af
--- /dev/null
+++ b/test/fixtures/wildebeest-well-known-nodeinfo.json
@@ -0,0 +1 @@
+{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://wildebeest.example.org/nodeinfo/2.0"},{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://wildebeest.example.org/nodeinfo/2.1"}]}
\ No newline at end of file
diff --git a/test/fixtures/xml_billion_laughs.xml b/test/fixtures/xml_billion_laughs.xml
new file mode 100644
index 000000000..75fb24cae
--- /dev/null
+++ b/test/fixtures/xml_billion_laughs.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+]>
+&lol9;
diff --git a/test/fixtures/xml_external_entities.xml b/test/fixtures/xml_external_entities.xml
new file mode 100644
index 000000000..d5ff87134
--- /dev/null
+++ b/test/fixtures/xml_external_entities.xml
@@ -0,0 +1,3 @@
+
+ ]>
+&xxe;
diff --git a/test/mix/tasks/pleroma/openapi_spec_test.exs b/test/mix/tasks/pleroma/openapi_spec_test.exs
new file mode 100644
index 000000000..01437187a
--- /dev/null
+++ b/test/mix/tasks/pleroma/openapi_spec_test.exs
@@ -0,0 +1,62 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.OpenapiSpecTest do
+ use Pleroma.DataCase, async: true
+
+ alias Mix.Tasks.Pleroma.OpenapiSpec
+
+ @spec_base %{
+ "paths" => %{
+ "/cofe" => %{
+ "get" => %{
+ "operationId" => "Some.operation",
+ "tags" => []
+ }
+ },
+ "/mew" => %{
+ "post" => %{
+ "operationId" => "Another.operation",
+ "tags" => ["mew mew"]
+ }
+ }
+ },
+ "x-tagGroups" => [
+ %{
+ "name" => "mew",
+ "tags" => ["mew mew", "abc"]
+ },
+ %{
+ "name" => "lol",
+ "tags" => ["lol lol", "xyz"]
+ }
+ ]
+ }
+
+ describe "check_specs/1" do
+ test "Every operation must have a tag" do
+ assert {:error, ["Some.operation (get /cofe): No tags specified"]} ==
+ OpenapiSpec.check_specs(@spec_base)
+ end
+
+ test "Every tag must be in tag groups" do
+ spec =
+ @spec_base
+ |> put_in(["paths", "/cofe", "get", "tags"], ["abc", "def", "not specified"])
+
+ assert {:error,
+ [
+ "Some.operation (get /cofe): Tags #{inspect(["def", "not specified"])} not available. Please add it in \"x-tagGroups\" in Pleroma.Web.ApiSpec"
+ ]} == OpenapiSpec.check_specs(spec)
+ end
+
+ test "No errors if ok" do
+ spec =
+ @spec_base
+ |> put_in(["paths", "/cofe", "get", "tags"], ["abc", "mew mew"])
+
+ assert :ok == OpenapiSpec.check_specs(spec)
+ end
+ end
+end
diff --git a/test/pleroma/bbs/handler_test.exs b/test/pleroma/bbs/handler_test.exs
deleted file mode 100644
index aea3b6ead..000000000
--- a/test/pleroma/bbs/handler_test.exs
+++ /dev/null
@@ -1,89 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.BBS.HandlerTest do
- use Pleroma.DataCase, async: true
- alias Pleroma.Activity
- alias Pleroma.BBS.Handler
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.CommonAPI
-
- import ExUnit.CaptureIO
- import Pleroma.Factory
- import Ecto.Query
-
- test "getting the home timeline" do
- user = insert(:user)
- followed = insert(:user)
-
- {:ok, user, followed} = User.follow(user, followed)
-
- {:ok, _first} = CommonAPI.post(user, %{status: "hey"})
- {:ok, _second} = CommonAPI.post(followed, %{status: "hello"})
-
- output =
- capture_io(fn ->
- Handler.handle_command(%{user: user}, "home")
- end)
-
- assert output =~ user.nickname
- assert output =~ followed.nickname
-
- assert output =~ "hey"
- assert output =~ "hello"
- end
-
- test "posting" do
- user = insert(:user)
-
- output =
- capture_io(fn ->
- Handler.handle_command(%{user: user}, "p this is a test post")
- end)
-
- assert output =~ "Posted"
-
- activity =
- Repo.one(
- from(a in Activity,
- where: fragment("?->>'type' = ?", a.data, "Create")
- )
- )
-
- assert activity.actor == user.ap_id
- object = Object.normalize(activity, fetch: false)
- assert object.data["content"] == "this is a test post"
- end
-
- test "replying" do
- user = insert(:user)
- another_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(another_user, %{status: "this is a test post"})
- activity_object = Object.normalize(activity, fetch: false)
-
- output =
- capture_io(fn ->
- Handler.handle_command(%{user: user}, "r #{activity.id} this is a reply")
- end)
-
- assert output =~ "Replied"
-
- reply =
- Repo.one(
- from(a in Activity,
- where: fragment("?->>'type' = ?", a.data, "Create"),
- where: a.actor == ^user.ap_id
- )
- )
-
- assert reply.actor == user.ap_id
-
- reply_object_data = Object.normalize(reply, fetch: false).data
- assert reply_object_data["content"] == "this is a reply"
- assert reply_object_data["inReplyTo"] == activity_object.data["id"]
- end
-end
diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs
index 4e0d4c838..8d2a93d6c 100644
--- a/test/pleroma/config/release_runtime_provider_test.exs
+++ b/test/pleroma/config/release_runtime_provider_test.exs
@@ -10,13 +10,15 @@ defmodule Pleroma.Config.ReleaseRuntimeProviderTest do
describe "load/2" do
test "loads release defaults config and warns about non-existent runtime config" do
ExUnit.CaptureIO.capture_io(fn ->
- merged = ReleaseRuntimeProvider.load([], [])
+ merged = ReleaseRuntimeProvider.load([], config_path: "/var/empty/config.exs")
assert merged == Pleroma.Config.Holder.release_defaults()
end) =~
"!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file"
end
test "merged runtime config" do
+ assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640)
+
merged =
ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs")
@@ -25,6 +27,8 @@ test "merged runtime config" do
end
test "merged exported config" do
+ assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640)
+
ExUnit.CaptureIO.capture_io(fn ->
merged =
ReleaseRuntimeProvider.load([],
@@ -37,6 +41,9 @@ test "merged exported config" do
end
test "runtime config is merged with exported config" do
+ assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640)
+ assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640)
+
merged =
ReleaseRuntimeProvider.load([],
config_path: "test/fixtures/config/temp.secret.exs",
diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs
new file mode 100644
index 000000000..760ecb465
--- /dev/null
+++ b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUriTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri
+
+ test "diaspora://" do
+ text = "diaspora://alice@fediverse.example/post/deadbeefdeadbeefdeadbeefdeadbeef"
+ assert {:ok, ^text} = BareUri.cast(text)
+ end
+
+ test "nostr:" do
+ text = "nostr:note1gwdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
+ assert {:ok, ^text} = BareUri.cast(text)
+ end
+
+ test "errors for non-URIs" do
+ assert :error == BareUri.cast(1)
+ assert :error == BareUri.cast("foo")
+ assert :error == BareUri.cast("foo bar")
+ end
+end
diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs
index 18b99da75..00001abfc 100644
--- a/test/pleroma/emoji/pack_test.exs
+++ b/test/pleroma/emoji/pack_test.exs
@@ -90,4 +90,8 @@ test "add emoji file", %{pack: pack} do
assert updated_pack.files_count == 1
end
+
+ test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do
+ assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack")
+ end
end
diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs
index 861519bce..a769f9362 100644
--- a/test/pleroma/instances/instance_test.exs
+++ b/test/pleroma/instances/instance_test.exs
@@ -161,6 +161,66 @@ test "Doesn't scrapes unreachable instances" do
end
end
+ describe "get_or_update_metadata/1" do
+ test "Scrapes Wildebeest NodeInfo" do
+ Tesla.Mock.mock(fn
+ %{url: "https://wildebeest.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/wildebeest-well-known-nodeinfo.json")
+ }
+
+ %{url: "https://wildebeest.example.org/nodeinfo/2.1"} ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/wildebeest-nodeinfo21.json")
+ }
+ end)
+
+ expected = %{
+ software_name: "wildebeest",
+ software_repository: "https://github.com/cloudflare/wildebeest",
+ software_version: "0.0.1"
+ }
+
+ assert expected ==
+ Instance.get_or_update_metadata(URI.parse("https://wildebeest.example.org/"))
+
+ expected = %Pleroma.Instances.Instance.Pleroma.Instances.Metadata{
+ software_name: "wildebeest",
+ software_repository: "https://github.com/cloudflare/wildebeest",
+ software_version: "0.0.1"
+ }
+
+ assert expected ==
+ Repo.get_by(Pleroma.Instances.Instance, %{host: "wildebeest.example.org"}).metadata
+ end
+
+ test "Scrapes Mastodon NodeInfo" do
+ Tesla.Mock.mock(fn
+ %{url: "https://mastodon.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/mastodon-well-known-nodeinfo.json")
+ }
+
+ %{url: "https://mastodon.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/mastodon-nodeinfo20.json")
+ }
+ end)
+
+ expected = %{
+ software_name: "mastodon",
+ software_version: "4.1.0"
+ }
+
+ assert expected ==
+ Instance.get_or_update_metadata(URI.parse("https://mastodon.example.org/"))
+ end
+ end
+
test "delete_users_and_activities/1 deletes remote instance users and activities" do
[mario, luigi, _peach, wario] =
users = [
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 9be0445c0..a2c20f0a6 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -31,9 +31,22 @@ def start_socket(qs \\ nil, headers \\ []) do
WebsocketClient.start_link(self(), path, headers)
end
+ defp decode_json(json) do
+ with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
+ {:ok, payload} <- Jason.decode(payload_text) do
+ {:ok, %{"event" => event, "payload" => payload}}
+ end
+ end
+
+ # Turns atom keys to strings
+ defp atom_key_to_string(json) do
+ json
+ |> Jason.encode!()
+ |> Jason.decode!()
+ end
+
test "refuses invalid requests" do
capture_log(fn ->
- assert {:error, %WebSockex.RequestError{code: 404}} = start_socket()
assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk")
Process.sleep(30)
end)
@@ -49,6 +62,10 @@ test "requires authentication and a valid token for protected streams" do
end)
end
+ test "allows unified stream" do
+ assert {:ok, _} = start_socket()
+ end
+
test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local")
@@ -70,12 +87,143 @@ test "receives well formatted events" do
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
- |> Jason.encode!()
- |> Jason.decode!()
+ |> atom_key_to_string()
assert json == view_json
end
+ describe "subscribing via WebSocket" do
+ test "can subscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, json} = Jason.decode(raw_json)
+
+ assert "update" == json["event"]
+ assert json["payload"]
+ assert {:ok, json} = Jason.decode(json["payload"])
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert json == view_json
+ end
+
+ test "can subscribe to multiple streams" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json)
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json)
+
+ streams = [stream1, stream2]
+ assert ["hashtag", "mew"] in streams
+ assert ["public"] in streams
+ end
+
+ test "won't double subscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "ignored"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+
+ assert_receive {:text, _}, 1_000
+ refute_receive {:text, _}, 1_000
+ end
+
+ test "rejects invalid streams" do
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "nonsense"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "error", "error" => "bad_topic"}
+ }} = decode_json(raw_json)
+ end
+
+ test "can unsubscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "unsubscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "unsubscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+ refute_receive {:text, _}, 1_000
+ end
+ end
+
describe "with a valid user token" do
setup do
{:ok, app} =
@@ -131,6 +279,124 @@ test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
end)
end
+ test "accepts valid token on client-sent event", %{token: token} do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+ end
+
+ test "rejects invalid token on client-sent event" do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{
+ "type" => "pleroma:authenticate",
+ "result" => "error",
+ "error" => "unauthorized"
+ }
+ }} = decode_json(raw_json)
+ end
+
+ test "rejects new authenticate request if already logged-in", %{token: token} do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{
+ "type" => "pleroma:authenticate",
+ "result" => "error",
+ "error" => "already_authenticated"
+ }
+ }} = decode_json(raw_json)
+ end
+
+ test "accepts the 'list' stream", %{token: token, user: user} do
+ posting_user = insert(:user)
+
+ {:ok, list} = Pleroma.List.create("test", user)
+ Pleroma.List.follow(list, posting_user)
+
+ assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
+
+ assert {:ok, pid} = start_socket("?access_token=#{token.token}")
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "list", list: list.id} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "list", list: to_string(list.id)} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "ignored"}
+ }} = decode_json(raw_json)
+ end
+
test "disconnect when token is revoked", %{app: app, user: user, token: token} do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
@@ -146,5 +412,85 @@ test "disconnect when token is revoked", %{app: app, user: user, token: token} d
assert_receive {:close, _}
refute_receive {:close, _}
end
+
+ test "receives private statuses", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, json} = Jason.decode(raw_json)
+
+ assert "update" == json["event"]
+ assert json["payload"]
+ assert {:ok, json} = Jason.decode(json["payload"])
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert json == view_json
+ end
+
+ test "receives edits", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
+
+ assert_receive {:text, _raw_json}, 1_000
+
+ {:ok, _} = CommonAPI.update(user, activity, %{status: "mew mew", visibility: "private"})
+
+ assert_receive {:text, raw_json}, 1_000
+
+ activity = Pleroma.Activity.normalize(activity)
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
+ end
+
+ test "receives notifications", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
+
+ {:ok, %Pleroma.Activity{id: activity_id} = _activity} =
+ CommonAPI.post(user, %{
+ status: "nice echo chamber @#{reading_user.nickname}",
+ visibility: "private"
+ })
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "notification",
+ "payload" => %{
+ "status" => %{
+ "id" => ^activity_id
+ }
+ }
+ }} = decode_json(raw_json)
+ end
end
end
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index a000c0efd..71af9acb8 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
import Pleroma.Factory
import Mock
@@ -32,20 +32,26 @@ test "never returns nil" do
refute {:ok, [nil]} == Notification.create_notifications(activity)
end
- test "creates a notification for a report" do
+ test "creates a report notification only for privileged users" do
reporting_user = insert(:user)
reported_user = insert(:user)
- {:ok, moderator_user} = insert(:user) |> User.admin_api_update(%{is_moderator: true})
+ moderator_user = insert(:user, is_moderator: true)
- {:ok, activity} = CommonAPI.report(reporting_user, %{account_id: reported_user.id})
+ clear_config([:instance, :moderator_privileges], [])
+ {:ok, activity1} = CommonAPI.report(reporting_user, %{account_id: reported_user.id})
+ {:ok, []} = Notification.create_notifications(activity1)
- {:ok, [notification]} = Notification.create_notifications(activity)
+ clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
+ {:ok, activity2} = CommonAPI.report(reporting_user, %{account_id: reported_user.id})
+ {:ok, [notification]} = Notification.create_notifications(activity2)
assert notification.user_id == moderator_user.id
assert notification.type == "pleroma:report"
end
- test "suppresses notification to reporter if reporter is an admin" do
+ test "suppresses notifications for own reports" do
+ clear_config([:instance, :admin_privileges], [:reports_manage_reports])
+
reporting_admin = insert(:user, is_admin: true)
reported_user = insert(:user)
other_admin = insert(:user, is_admin: true)
@@ -246,7 +252,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
task =
Task.async(fn ->
{:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
task_user_notification =
@@ -254,7 +260,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
{:ok, _topic} =
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
activity = insert(:note_activity)
@@ -328,6 +334,32 @@ test "it disables notifications from strangers" do
refute Notification.create_notification(activity, followed)
end
+ test "it disables notifications from non-followees" do
+ follower = insert(:user)
+
+ followed =
+ insert(:user,
+ notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
+ )
+
+ CommonAPI.follow(follower, followed)
+ {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
+ refute Notification.create_notification(activity, followed)
+ end
+
+ test "it allows notifications from followees" do
+ poster = insert(:user)
+
+ receiver =
+ insert(:user,
+ notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
+ )
+
+ CommonAPI.follow(receiver, poster)
+ {:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"})
+ assert Notification.create_notification(activity, receiver)
+ end
+
test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
@@ -542,25 +574,6 @@ test "it clears all notifications belonging to the user" do
end
end
- describe "destroy_multiple_from_types/2" do
- test "clears all notifications of a certain type for a given user" do
- report_activity = insert(:report_activity)
- user1 = insert(:user, is_moderator: true, is_admin: true)
- user2 = insert(:user, is_moderator: true, is_admin: true)
- {:ok, _} = Notification.create_notifications(report_activity)
-
- {:ok, _} =
- CommonAPI.post(user2, %{
- status: "hey @#{user1.nickname} !"
- })
-
- Notification.destroy_multiple_from_types(user1, ["pleroma:report"])
-
- assert [%Pleroma.Notification{type: "mention"}] = Notification.for_user(user1)
- assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user2)
- end
- end
-
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
user = insert(:user)
@@ -1238,5 +1251,32 @@ test "it returns notifications about favorites with filtered word", %{user: user
assert length(Notification.for_user(user)) == 1
end
+
+ test "it returns notifications when related object is without content and filters are defined",
+ %{user: user} do
+ followed_user = insert(:user, is_locked: true)
+
+ insert(:filter, user: followed_user, phrase: "test", hide: true)
+
+ {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
+ refute FollowingRelationship.following?(user, followed_user)
+ assert [notification] = Notification.for_user(followed_user)
+
+ assert %{type: "follow_request"} =
+ NotificationView.render("show.json", %{
+ notification: notification,
+ for: followed_user
+ })
+
+ assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
+
+ assert [notification] = Notification.for_user(followed_user)
+
+ assert %{type: "follow"} =
+ NotificationView.render("show.json", %{
+ notification: notification,
+ for: followed_user
+ })
+ end
end
end
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index c8ad66ddb..53c9277d6 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -9,8 +9,12 @@ defmodule Pleroma.Object.FetcherTest do
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Object.Fetcher
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+
+ require Pleroma.Constants
import Mock
+ import Pleroma.Factory
import Tesla.Mock
setup do
@@ -284,6 +288,8 @@ test "it can refetch pruned objects" do
describe "refetching" do
setup do
+ insert(:user, ap_id: "https://mastodon.social/users/emelie")
+
object1 = %{
"id" => "https://mastodon.social/1",
"actor" => "https://mastodon.social/users/emelie",
@@ -293,10 +299,14 @@ test "it can refetch pruned objects" do
"bcc" => [],
"bto" => [],
"cc" => [],
- "to" => [],
- "summary" => ""
+ "to" => [Pleroma.Constants.as_public()],
+ "summary" => "",
+ "published" => "2023-05-08 23:43:20Z",
+ "updated" => "2023-05-09 23:43:20Z"
}
+ {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
+
object2 = %{
"id" => "https://mastodon.social/2",
"actor" => "https://mastodon.social/users/emelie",
@@ -306,8 +316,10 @@ test "it can refetch pruned objects" do
"bcc" => [],
"bto" => [],
"cc" => [],
- "to" => [],
+ "to" => [Pleroma.Constants.as_public()],
"summary" => "",
+ "published" => "2023-05-08 23:43:20Z",
+ "updated" => "2023-05-09 23:43:25Z",
"formerRepresentations" => %{
"type" => "OrderedCollection",
"orderedItems" => [
@@ -319,14 +331,18 @@ test "it can refetch pruned objects" do
"bcc" => [],
"bto" => [],
"cc" => [],
- "to" => [],
- "summary" => ""
+ "to" => [Pleroma.Constants.as_public()],
+ "summary" => "",
+ "published" => "2023-05-08 23:43:20Z",
+ "updated" => "2023-05-09 23:43:21Z"
}
],
"totalItems" => 1
}
}
+ {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
+
mock(fn
%{
method: :get,
@@ -335,7 +351,7 @@ test "it can refetch pruned objects" do
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
- body: Jason.encode!(object1)
+ body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
}
%{
@@ -345,7 +361,7 @@ test "it can refetch pruned objects" do
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
- body: Jason.encode!(object2)
+ body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
}
%{
@@ -370,7 +386,7 @@ test "it can refetch pruned objects" do
apply(HttpRequestMock, :request, [env])
end)
- %{object1: object1, object2: object2}
+ %{object1: local_object1, object2: local_object2}
end
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
@@ -388,8 +404,9 @@ test "it keeps formerRepresentations if remote does not have this attr", %{objec
"bcc" => [],
"bto" => [],
"cc" => [],
- "to" => [],
- "summary" => ""
+ "to" => [Pleroma.Constants.as_public()],
+ "summary" => "",
+ "published" => "2023-05-08 23:43:20Z"
}
],
"totalItems" => 1
@@ -467,6 +484,53 @@ test "it adds to formerRepresentations if the remote does not have one and the o
}
} = refetched.data
end
+
+ test "it keeps the history intact if only updated time has changed",
+ %{object1: object1} do
+ full_object1 =
+ object1
+ |> Map.merge(%{
+ "updated" => "2023-05-08 23:43:47Z",
+ "formerRepresentations" => %{
+ "type" => "OrderedCollection",
+ "orderedItems" => [
+ %{"type" => "Note", "content" => "mew mew 1"}
+ ],
+ "totalItems" => 1
+ }
+ })
+
+ {:ok, o} = Object.create(full_object1)
+
+ assert {:ok, refetched} = Fetcher.refetch_object(o)
+
+ assert %{
+ "content" => "test 1",
+ "formerRepresentations" => %{
+ "orderedItems" => [
+ %{"content" => "mew mew 1"}
+ ],
+ "totalItems" => 1
+ }
+ } = refetched.data
+ end
+
+ test "it goes through ObjectValidator and MRF", %{object2: object2} do
+ with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
+ filter: fn
+ %{"type" => "Note"} = object ->
+ {:ok, Map.put(object, "content", "MRFd content")}
+
+ arg ->
+ passthrough([arg])
+ end do
+ {:ok, o} = Object.create(object2)
+
+ assert {:ok, refetched} = Fetcher.refetch_object(o)
+
+ assert %{"content" => "MRFd content"} = refetched.data
+ end
+ end
end
describe "fetch with history" do
diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs
index d536e0b16..7bc5c9928 100644
--- a/test/pleroma/object_test.exs
+++ b/test/pleroma/object_test.exs
@@ -444,4 +444,42 @@ test "Hashtag records are created with Object record and updated on its change"
Enum.sort_by(object.hashtags, & &1.name)
end
end
+
+ describe "get_emoji_reactions/1" do
+ test "3-tuple current format" do
+ object = %Object{
+ data: %{
+ "reactions" => [
+ ["x", ["https://some/user"], "https://some/emoji"]
+ ]
+ }
+ }
+
+ assert Object.get_emoji_reactions(object) == object.data["reactions"]
+ end
+
+ test "2-tuple legacy format" do
+ object = %Object{
+ data: %{
+ "reactions" => [
+ ["x", ["https://some/user"]]
+ ]
+ }
+ }
+
+ assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
+ end
+
+ test "Map format" do
+ object = %Object{
+ data: %{
+ "reactions" => %{
+ "x" => ["https://some/user"]
+ }
+ }
+ }
+
+ assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
+ end
+ end
end
diff --git a/test/pleroma/upload/filter/exiftool/read_description_test.exs b/test/pleroma/upload/filter/exiftool/read_description_test.exs
index 7389fda47..7cc83969d 100644
--- a/test/pleroma/upload/filter/exiftool/read_description_test.exs
+++ b/test/pleroma/upload/filter/exiftool/read_description_test.exs
@@ -42,6 +42,33 @@ test "otherwise returns ImageDescription when present" do
{:ok, :filtered, uploads_after}
end
+ test "Ignores warnings" do
+ uploads = %Pleroma.Upload{
+ name: "image_with_imagedescription_and_caption-abstract_and_stray_data_after.png",
+ content_type: "image/png",
+ path:
+ Path.absname(
+ "test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
+ ),
+ tempfile:
+ Path.absname(
+ "test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
+ )
+ }
+
+ assert {:ok, :filtered, %{description: "a descriptive white pixel"}} =
+ Filter.Exiftool.ReadDescription.filter(uploads)
+
+ uploads = %Pleroma.Upload{
+ name: "image_with_stray_data_after.png",
+ content_type: "image/png",
+ path: Path.absname("test/fixtures/image_with_stray_data_after.png"),
+ tempfile: Path.absname("test/fixtures/image_with_stray_data_after.png")
+ }
+
+ assert {:ok, :filtered, %{description: nil}} = Filter.Exiftool.ReadDescription.filter(uploads)
+ end
+
test "otherwise returns iptc:Caption-Abstract when present" do
upload = %Pleroma.Upload{
name: "image_with_caption-abstract.jpg",
diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs
index 7e1541f60..bcb5f3f60 100644
--- a/test/pleroma/upload/filter/exiftool/strip_location_test.exs
+++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs
@@ -31,12 +31,19 @@ test "apply exiftool filter" do
refute String.match?(exif_filtered, ~r/GPS/)
end
- test "verify webp files are skipped" do
- upload = %Pleroma.Upload{
- name: "sample.webp",
- content_type: "image/webp"
- }
+ test "verify webp, heic, svg files are skipped" do
+ uploads =
+ ~w{webp heic svg svg+xml}
+ |> Enum.map(fn type ->
+ %Pleroma.Upload{
+ name: "sample.#{type}",
+ content_type: "image/#{type}"
+ }
+ end)
- assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop}
+ uploads
+ |> Enum.each(fn upload ->
+ assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop}
+ end)
end
end
diff --git a/test/pleroma/upload/filter/only_media_test.exs b/test/pleroma/upload/filter/only_media_test.exs
new file mode 100644
index 000000000..75be070a1
--- /dev/null
+++ b/test/pleroma/upload/filter/only_media_test.exs
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.OnlyMediaTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Upload
+ alias Pleroma.Upload.Filter.OnlyMedia
+
+ test "Allows media Content-Type" do
+ ["audio/mpeg", "image/jpeg", "video/mp4"]
+ |> Enum.each(fn type ->
+ upload = %Upload{
+ content_type: type
+ }
+
+ assert {:ok, :noop} = OnlyMedia.filter(upload)
+ end)
+ end
+
+ test "Disallows non-media Content-Type" do
+ ["application/javascript", "application/pdf", "text/html"]
+ |> Enum.each(fn type ->
+ upload = %Upload{
+ content_type: type
+ }
+
+ assert {:error, _} = OnlyMedia.filter(upload)
+ end)
+ end
+end
diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs
index 5c9b94000..066bf6ba8 100644
--- a/test/pleroma/user/backup_test.exs
+++ b/test/pleroma/user/backup_test.exs
@@ -39,7 +39,7 @@ test "it creates a backup record and an Oban job" do
assert_enqueued(worker: BackupWorker, args: args)
backup = Backup.get(args["backup_id"])
- assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup
+ assert %Backup{user_id: ^user_id, processed: false, file_size: 0, state: :pending} = backup
end
test "it return an error if the export limit is over" do
@@ -59,7 +59,30 @@ test "it process a backup record" do
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
assert {:ok, backup} = perform_job(BackupWorker, args)
assert backup.file_size > 0
- assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup
+ assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup
+
+ delete_job_args = %{"op" => "delete", "backup_id" => backup_id}
+
+ assert_enqueued(worker: BackupWorker, args: delete_job_args)
+ assert {:ok, backup} = perform_job(BackupWorker, delete_job_args)
+ refute Backup.get(backup_id)
+
+ email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup)
+
+ assert_email_sent(
+ to: {user.name, user.email},
+ html_body: email.html_body
+ )
+ end
+
+ test "it updates states of the backup" do
+ clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+ %{id: user_id} = user = insert(:user)
+
+ assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
+ assert {:ok, backup} = perform_job(BackupWorker, args)
+ assert backup.file_size > 0
+ assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup
delete_job_args = %{"op" => "delete", "backup_id" => backup_id}
@@ -148,7 +171,7 @@ test "it creates a zip archive with user data" do
Bookmark.create(user.id, status3.id)
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
- assert {:ok, path} = Backup.export(backup)
+ assert {:ok, path} = Backup.export(backup, self())
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile)
@@ -230,6 +253,73 @@ test "it creates a zip archive with user data" do
File.rm!(path)
end
+ test "it counts the correct number processed" do
+ user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
+
+ Enum.map(1..120, fn i ->
+ {:ok, status} = CommonAPI.post(user, %{status: "status #{i}"})
+ CommonAPI.favorite(user, status.id)
+ Bookmark.create(user.id, status.id)
+ end)
+
+ assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
+ {:ok, backup} = Backup.process(backup)
+
+ assert backup.processed_number == 1 + 120 + 120 + 120
+
+ Backup.delete(backup)
+ end
+
+ test "it handles errors" do
+ user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
+
+ Enum.map(1..120, fn i ->
+ {:ok, _status} = CommonAPI.post(user, %{status: "status #{i}"})
+ end)
+
+ assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
+
+ with_mock Pleroma.Web.ActivityPub.Transmogrifier,
+ [:passthrough],
+ prepare_outgoing: fn data ->
+ object =
+ data["object"]
+ |> Pleroma.Object.normalize(fetch: false)
+ |> Map.get(:data)
+
+ data = data |> Map.put("object", object)
+
+ if String.contains?(data["object"]["content"], "119"),
+ do: raise(%Postgrex.Error{}),
+ else: {:ok, data}
+ end do
+ {:ok, backup} = Backup.process(backup)
+ assert backup.processed
+ assert backup.state == :complete
+ assert backup.processed_number == 1 + 119
+
+ Backup.delete(backup)
+ end
+ end
+
+ test "it handles unrecoverable exceptions" do
+ user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
+
+ assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
+
+ with_mock Backup, [:passthrough], do_process: fn _, _ -> raise "mock exception" end do
+ {:error, %{backup: backup, reason: :exit}} = Backup.process(backup)
+
+ assert backup.state == :failed
+ end
+
+ with_mock Backup, [:passthrough], do_process: fn _, _ -> Process.sleep(:timer.seconds(32)) end do
+ {:error, %{backup: backup, reason: :timeout}} = Backup.process(backup)
+
+ assert backup.state == :failed
+ end
+ end
+
describe "it uploads and deletes a backup archive" do
setup do
clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com")
@@ -246,7 +336,7 @@ test "it creates a zip archive with user data" do
Bookmark.create(user.id, status3.id)
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
- assert {:ok, path} = Backup.export(backup)
+ assert {:ok, path} = Backup.export(backup, self())
[path: path, backup: backup]
end
diff --git a/test/pleroma/user/import_test.exs b/test/pleroma/user/import_test.exs
index b4efd4bb0..f75305e0e 100644
--- a/test/pleroma/user/import_test.exs
+++ b/test/pleroma/user/import_test.exs
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.ImportTest do
- alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
diff --git a/test/pleroma/user/query_test.exs b/test/pleroma/user/query_test.exs
index bd45d1bca..30a4637f2 100644
--- a/test/pleroma/user/query_test.exs
+++ b/test/pleroma/user/query_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.QueryTest do
- use Pleroma.DataCase, async: true
+ use Pleroma.DataCase, async: false
alias Pleroma.Repo
alias Pleroma.User
@@ -44,4 +44,63 @@ test "is_suggested param" do
|> User.Query.build()
|> Repo.all()
end
+
+ describe "is_privileged param" do
+ setup do
+ %{
+ user: insert(:user, local: true, is_admin: false, is_moderator: false),
+ moderator_user: insert(:user, local: true, is_admin: false, is_moderator: true),
+ admin_user: insert(:user, local: true, is_admin: true, is_moderator: false),
+ admin_moderator_user: insert(:user, local: true, is_admin: true, is_moderator: true),
+ remote_user: insert(:user, local: false, is_admin: true, is_moderator: true),
+ non_active_user:
+ insert(:user, local: true, is_admin: true, is_moderator: true, is_active: false)
+ }
+ end
+
+ test "doesn't return any users when there are no privileged roles" do
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [])
+
+ assert [] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ end
+
+ test "returns moderator users if they are privileged", %{
+ moderator_user: moderator_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [:cofe])
+
+ assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ end
+
+ test "returns admin users if they are privileged", %{
+ admin_user: admin_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ clear_config([:instance, :moderator_privileges], [])
+
+ assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert admin_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ end
+
+ test "returns admin and moderator users if they are both privileged", %{
+ moderator_user: moderator_user,
+ admin_user: admin_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ clear_config([:instance, :moderator_privileges], [:cofe])
+
+ assert [_, _, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert admin_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ assert moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all())
+ end
+ end
end
diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs
index 1deab6888..1af9a1493 100644
--- a/test/pleroma/user_search_test.exs
+++ b/test/pleroma/user_search_test.exs
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserSearchTest do
- alias Pleroma.Repo
alias Pleroma.User
use Pleroma.DataCase
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index a58c8f68b..7f60b959a 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -5,7 +5,6 @@
defmodule Pleroma.UserTest do
alias Pleroma.Activity
alias Pleroma.Builders.UserBuilder
- alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
@@ -13,7 +12,7 @@ defmodule Pleroma.UserTest do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
@@ -473,12 +472,7 @@ test "it sends a welcome chat message if it is set" do
reject_deletes: []
)
- setup do:
- clear_config(:mrf,
- policies: [
- Pleroma.Web.ActivityPub.MRF.SimplePolicy
- ]
- )
+ setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
test "it sends a welcome chat message when Simple policy applied to local instance" do
clear_config([:mrf_simple, :media_nsfw], [{"localhost", ""}])
@@ -1850,7 +1844,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
confirmation_token: "qqqq",
domain_blocks: ["lain.com"],
is_active: false,
- ap_enabled: true,
is_moderator: true,
is_admin: true,
mascot: %{"a" => "b"},
@@ -1891,7 +1884,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
confirmation_token: nil,
domain_blocks: [],
is_active: false,
- ap_enabled: false,
is_moderator: false,
is_admin: false,
mascot: nil,
@@ -2038,31 +2030,82 @@ test "returns :approval_pending for unapproved user" do
end
end
- describe "superuser?/1" do
+ describe "privileged?/1" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:cofe, :suya])
+ clear_config([:instance, :moderator_privileges], [:cofe, :suya])
+ end
+
test "returns false for unprivileged users" do
user = insert(:user, local: true)
- refute User.superuser?(user)
+ refute User.privileged?(user, :cofe)
end
test "returns false for remote users" do
user = insert(:user, local: false)
remote_admin_user = insert(:user, local: false, is_admin: true)
- refute User.superuser?(user)
- refute User.superuser?(remote_admin_user)
+ refute User.privileged?(user, :cofe)
+ refute User.privileged?(remote_admin_user, :cofe)
end
- test "returns true for local moderators" do
+ test "returns true for local moderators if, and only if, they are privileged" do
user = insert(:user, local: true, is_moderator: true)
- assert User.superuser?(user)
+ assert User.privileged?(user, :cofe)
+
+ clear_config([:instance, :moderator_privileges], [])
+
+ refute User.privileged?(user, :cofe)
end
- test "returns true for local admins" do
+ test "returns true for local admins if, and only if, they are privileged" do
user = insert(:user, local: true, is_admin: true)
- assert User.superuser?(user)
+ assert User.privileged?(user, :cofe)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ refute User.privileged?(user, :cofe)
+ end
+ end
+
+ describe "privileges/1" do
+ setup do
+ clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator])
+ clear_config([:instance, :admin_privileges], [:cofe, :only_admin])
+ end
+
+ test "returns empty list for users without roles" do
+ user = insert(:user, local: true)
+
+ assert [] == User.privileges(user)
+ end
+
+ test "returns list of privileges for moderators" do
+ moderator = insert(:user, is_moderator: true, local: true)
+
+ assert [:cofe, :only_moderator] == User.privileges(moderator) |> Enum.sort()
+ end
+
+ test "returns list of privileges for admins" do
+ admin = insert(:user, is_admin: true, local: true)
+
+ assert [:cofe, :only_admin] == User.privileges(admin) |> Enum.sort()
+ end
+
+ test "returns list of unique privileges for users who are both moderator and admin" do
+ moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: true)
+
+ assert [:cofe, :only_admin, :only_moderator] ==
+ User.privileges(moderator_admin) |> Enum.sort()
+ end
+
+ test "returns empty list for remote users" do
+ remote_moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: false)
+
+ assert [] == User.privileges(remote_moderator_admin)
end
end
@@ -2105,13 +2148,77 @@ test "returns true when the account is unconfirmed and confirmation is required
assert User.visible_for(user, other_user) == :visible
end
- test "returns true when the account is unconfirmed and being viewed by a privileged account (confirmation required)" do
+ test "returns true when the account is unconfirmed and being viewed by a privileged account (privilege :users_manage_activation_state, confirmation required)" do
clear_config([:instance, :account_activation_required], true)
+ clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
user = insert(:user, local: true, is_confirmed: false)
other_user = insert(:user, local: true, is_admin: true)
assert User.visible_for(user, other_user) == :visible
+
+ clear_config([:instance, :admin_privileges], [])
+
+ refute User.visible_for(user, other_user) == :visible
+ end
+ end
+
+ describe "all_users_with_privilege/1" do
+ setup do
+ %{
+ user: insert(:user, local: true, is_admin: false, is_moderator: false),
+ moderator_user: insert(:user, local: true, is_admin: false, is_moderator: true),
+ admin_user: insert(:user, local: true, is_admin: true, is_moderator: false),
+ admin_moderator_user: insert(:user, local: true, is_admin: true, is_moderator: true),
+ remote_user: insert(:user, local: false, is_admin: true, is_moderator: true),
+ non_active_user:
+ insert(:user, local: true, is_admin: true, is_moderator: true, is_active: false)
+ }
+ end
+
+ test "doesn't return any users when there are no privileged roles" do
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [])
+
+ assert [] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ end
+
+ test "returns moderator users if they are privileged", %{
+ moderator_user: moderator_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [:cofe])
+
+ assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert moderator_user in User.all_users_with_privilege(:cofe)
+ assert admin_moderator_user in User.all_users_with_privilege(:cofe)
+ end
+
+ test "returns admin users if they are privileged", %{
+ admin_user: admin_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ clear_config([:instance, :moderator_privileges], [])
+
+ assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert admin_user in User.all_users_with_privilege(:cofe)
+ assert admin_moderator_user in User.all_users_with_privilege(:cofe)
+ end
+
+ test "returns admin and moderator users if they are both privileged", %{
+ moderator_user: moderator_user,
+ admin_user: admin_user,
+ admin_moderator_user: admin_moderator_user
+ } do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ clear_config([:instance, :moderator_privileges], [:cofe])
+
+ assert [_, _, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
+ assert admin_user in User.all_users_with_privilege(:cofe)
+ assert moderator_user in User.all_users_with_privilege(:cofe)
+ assert admin_moderator_user in User.all_users_with_privilege(:cofe)
end
end
@@ -2351,26 +2458,6 @@ test "performs update cache if user updated" do
assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
end
-
- test "removes report notifs when user isn't superuser any more" do
- report_activity = insert(:report_activity)
- user = insert(:user, is_moderator: true, is_admin: true)
- {:ok, _} = Notification.create_notifications(report_activity)
-
- assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
-
- {:ok, user} = user |> User.admin_api_update(%{is_moderator: false})
- # is still superuser because still admin
- assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
-
- {:ok, user} = user |> User.admin_api_update(%{is_moderator: true, is_admin: false})
- # is still superuser because still moderator
- assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
-
- {:ok, user} = user |> User.admin_api_update(%{is_moderator: false})
- # is not a superuser any more
- assert [] = Notification.for_user(user)
- end
end
describe "following/followers synchronization" do
@@ -2384,8 +2471,7 @@ test "updates the counters normally on following/getting a follow when disabled"
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following",
- ap_enabled: true
+ following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
@@ -2406,8 +2492,7 @@ test "synchronizes the counters with the remote instance for the followed when e
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following",
- ap_enabled: true
+ following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
@@ -2428,8 +2513,7 @@ test "synchronizes the counters with the remote instance for the follower when e
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following",
- ap_enabled: true
+ following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index ef91066c1..62eb9b5a3 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -575,7 +575,6 @@ test "it inserts an incoming activity into the database" <>
user =
insert(:user,
ap_id: "https://mastodon.example.org/users/raymoo",
- ap_enabled: true,
local: false,
last_refreshed_at: nil
)
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
index fc6fc039d..1e8c14043 100644
--- a/test/pleroma/web/activity_pub/activity_pub_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -174,7 +174,6 @@ test "it returns a user" do
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
- assert user.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
@@ -1342,6 +1341,14 @@ test "returns reblogs for users for whom reblogs have not been muted" do
%{test_file: test_file}
end
+ test "strips / from filename", %{test_file: file} do
+ file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
+ {:ok, %Object{} = object} = ActivityPub.upload(file)
+ [%{"href" => href}] = object.data["url"]
+ assert Regex.match?(~r"/bad.jpg$", href)
+ refute Regex.match?(~r"/nested/", href)
+ end
+
test "sets a description if given", %{test_file: file} do
{:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
assert object.data["name"] == "a cool file"
@@ -2645,4 +2652,12 @@ test "allow fetching of accounts with an empty string name field" do
{:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew")
assert user.name == " "
end
+
+ test "pin_data_from_featured_collection will ignore unsupported values" do
+ assert %{} ==
+ ActivityPub.pin_data_from_featured_collection(%{
+ "type" => "OrderedCollection",
+ "first" => "https://social.example/users/alice/collections/featured?page=true"
+ })
+ end
end
diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs
index eb175a1be..52058a0a3 100644
--- a/test/pleroma/web/activity_pub/builder_test.exs
+++ b/test/pleroma/web/activity_pub/builder_test.exs
@@ -44,5 +44,34 @@ test "returns note data" do
assert {:ok, ^expected, []} = Builder.note(draft)
end
+
+ test "quote post" do
+ user = insert(:user)
+ note = insert(:note)
+
+ draft = %ActivityDraft{
+ user: user,
+ context: "2hu",
+ content_html: "This is :moominmamma: note ",
+ quote_post: note,
+ extra: %{}
+ }
+
+ expected = %{
+ "actor" => user.ap_id,
+ "attachment" => [],
+ "content" => "This is :moominmamma: note ",
+ "context" => "2hu",
+ "sensitive" => false,
+ "type" => "Note",
+ "quoteUrl" => note.data["id"],
+ "cc" => [],
+ "summary" => nil,
+ "tag" => [],
+ "to" => []
+ }
+
+ assert {:ok, ^expected, []} = Builder.note(draft)
+ end
end
end
diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs
new file mode 100644
index 000000000..7350800f0
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs
@@ -0,0 +1,425 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do
+ use Pleroma.DataCase
+
+ require Pleroma.Constants
+
+ alias Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Web.ActivityPub.MRF.EmojiPolicy
+
+ setup do: clear_config(:mrf_emoji)
+
+ setup do
+ clear_config([:mrf_emoji], %{
+ remove_url: [],
+ remove_shortcode: [],
+ federated_timeline_removal_url: [],
+ federated_timeline_removal_shortcode: []
+ })
+ end
+
+ @emoji_tags [
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/biribiri/mikoto_smile2.png"
+ },
+ "id" => "https://example.org/emoji/biribiri/mikoto_smile2.png",
+ "name" => ":mikoto_smile2:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/biribiri/mikoto_smile3.png"
+ },
+ "id" => "https://example.org/emoji/biribiri/mikoto_smile3.png",
+ "name" => ":mikoto_smile3:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ },
+ "id" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png",
+ "name" => ":nekomimi_girl_emoji_007:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/test.png"
+ },
+ "id" => "https://example.org/test.png",
+ "name" => ":test:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ }
+ ]
+
+ @misc_tags [%{"type" => "Placeholder"}]
+
+ @user_data %{
+ "type" => "Person",
+ "id" => "https://example.org/placeholder",
+ "name" => "lol",
+ "tag" => @emoji_tags ++ @misc_tags
+ }
+
+ @status_data %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://example.org/placeholder",
+ "content" => "lol",
+ "tag" => @emoji_tags ++ @misc_tags,
+ "emoji" => %{
+ "mikoto_smile2" => "https://example.org/emoji/biribiri/mikoto_smile2.png",
+ "mikoto_smile3" => "https://example.org/emoji/biribiri/mikoto_smile3.png",
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png",
+ "test" => "https://example.org/test.png"
+ },
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ },
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @status_data_with_history %{
+ "type" => "Create",
+ "object" =>
+ @status_data["object"]
+ |> Map.merge(%{
+ "formerRepresentations" => %{
+ "type" => "OrderedCollection",
+ "orderedItems" => [@status_data["object"] |> Map.put("content", "older")],
+ "totalItems" => 1
+ }
+ }),
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(3)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_matching_regex %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(1)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_matching_nothing %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(2)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_unicode %{
+ "type" => "EmojiReact",
+ "content" => "😍",
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ describe "remove_url" do
+ setup do
+ clear_config([:mrf_emoji, :remove_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes user" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ assert %{"tag" => ^expected_tags} = filtered
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered
+ end
+
+ test "processes status with history" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes updates" do
+ {:ok, filtered} =
+ MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update"))
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes EmojiReact" do
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data)
+
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex)
+
+ assert {:ok, @emoji_react_data_matching_nothing} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing)
+
+ assert {:ok, @emoji_react_data_unicode} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode)
+ end
+ end
+
+ describe "remove_shortcode" do
+ setup do
+ clear_config([:mrf_emoji, :remove_shortcode], [
+ "test",
+ ~r{mikoto_s},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes user" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ assert %{"tag" => ^expected_tags} = filtered
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered
+ end
+
+ test "processes status with history" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes updates" do
+ {:ok, filtered} =
+ MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update"))
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes EmojiReact" do
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data)
+
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex)
+
+ assert {:ok, @emoji_react_data_matching_nothing} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing)
+
+ assert {:ok, @emoji_react_data_unicode} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode)
+ end
+ end
+
+ describe "federated_timeline_removal_url" do
+ setup do
+ clear_config([:mrf_emoji, :federated_timeline_removal_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self"]
+ expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}
+ } = filtered
+ end
+
+ test "ignore updates" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data |> Map.put("type", "Update"))
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self", Pleroma.Constants.as_public()]
+ expected_cc = ["https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}
+ } = filtered
+ end
+
+ test "processes status with history" do
+ status =
+ @status_data_with_history
+ |> put_in(["object", "tag"], @misc_tags)
+ |> put_in(["object", "emoji"], %{})
+
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self"]
+ expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{
+ "formerRepresentations" => %{
+ "orderedItems" => [%{"tag" => ^expected_tags, "emoji" => ^expected_emoji}]
+ }
+ }
+ } = filtered
+ end
+ end
+
+ describe "edge cases" do
+ setup do
+ clear_config([:mrf_emoji, :remove_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "non-statuses" do
+ answer = @status_data |> put_in(["object", "type"], "Answer")
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, answer)
+
+ assert filtered == answer
+ end
+
+ test "without tag" do
+ status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["tag"]))
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ refute Map.has_key?(filtered["object"], "tag")
+ end
+
+ test "without emoji" do
+ status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["emoji"]))
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ refute Map.has_key?(filtered["object"], "emoji")
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
index b349a4bb7..811ef105c 100644
--- a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
@@ -256,4 +256,55 @@ test "works with Updates" do
}
}} = MRF.filter_one(ForceMentionsInContent, activity)
end
+
+ test "don't add duplicate mentions for mastodon or misskey posts" do
+ [zero, rogerick, greg] = [
+ insert(:user,
+ ap_id: "https://pleroma.example.com/users/zero",
+ uri: "https://pleroma.example.com/users/zero",
+ nickname: "zero@pleroma.example.com",
+ local: false
+ ),
+ insert(:user,
+ ap_id: "https://misskey.example.com/users/104ab42f11",
+ uri: "https://misskey.example.com/@rogerick",
+ nickname: "rogerick@misskey.example.com",
+ local: false
+ ),
+ insert(:user,
+ ap_id: "https://mastodon.example.com/users/greg",
+ uri: "https://mastodon.example.com/@greg",
+ nickname: "greg@mastodon.example.com",
+ local: false
+ )
+ ]
+
+ {:ok, post} = CommonAPI.post(rogerick, %{status: "eugh"})
+
+ inline_mentions = [
+ "@rogerick ",
+ "@greg "
+ ]
+
+ activity = %{
+ "type" => "Create",
+ "actor" => zero.ap_id,
+ "object" => %{
+ "type" => "Note",
+ "actor" => zero.ap_id,
+ "content" => "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm",
+ "to" => [
+ rogerick.ap_id,
+ greg.ap_id,
+ Constants.as_public()
+ ],
+ "inReplyTo" => Object.normalize(post).data["id"]
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity)
+
+ assert filtered ==
+ "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm"
+ end
end
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
new file mode 100644
index 000000000..d5762766f
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -0,0 +1,112 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
+ alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
+ use Pleroma.DataCase
+
+ test "adds quote URL to post content" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice postRT: https://gleasonator.com/objects/1234 "
+ end
+
+ test "adds quote URL to post content, custom template" do
+ clear_config([:mrf_inline_quote, :template], "{url}'s quoting")
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice posthttps://gleasonator.com/objects/1234 's quoting "
+ end
+
+ test "doesn't add line breaks to markdown posts" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post
",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice postRT: https://gleasonator.com/objects/1234
"
+ end
+
+ test "ignores Misskey quote posts" do
+ object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://misskey.io/users/7rkrarq81i",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
+
+ test "ignores Fedibird quote posts" do
+ object = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
+
+ # Normally the ObjectValidator will fix this before it reaches MRF
+ object = Map.put(object, "quoteUrl", object["quoteURL"])
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://fedibird.com/users/noellabo",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
+
+ test "skips objects which already have an .inline-quote span" do
+ object =
+ File.read!("test/fixtures/quote_post/fedibird_quote_mismatched.json") |> Jason.decode!()
+
+ # Normally the ObjectValidator will fix this before it reaches MRF
+ object = Map.put(object, "quoteUrl", object["quoteUri"])
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://fedibird.com/users/noellabo",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs
new file mode 100644
index 000000000..96b49b6a0
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicyTest do
+ alias Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy
+
+ use Pleroma.DataCase
+
+ require Pleroma.Constants
+
+ test "Add quote url to Link tag" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity)
+
+ assert object["tag"] == [
+ %{
+ "type" => "Link",
+ "href" => quote_url,
+ "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type()
+ }
+ ]
+ end
+
+ test "Add quote url to Link tag, append to the end" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url,
+ "tag" => [%{"type" => "Hashtag", "name" => "#foo"}]
+ }
+ }
+
+ {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity)
+
+ assert [_, tag] = object["tag"]
+
+ assert tag == %{
+ "type" => "Link",
+ "href" => quote_url,
+ "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type()
+ }
+ end
+
+ test "Bypass posts without quoteUrl" do
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post"
+ }
+ }
+
+ assert {:ok, ^activity} = QuoteToLinkTagPolicy.filter(activity)
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/utils_test.exs b/test/pleroma/web/activity_pub/mrf/utils_test.exs
new file mode 100644
index 000000000..3bbc2cfd3
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/utils_test.exs
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.UtilsTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Web.ActivityPub.MRF.Utils
+
+ describe "describe_regex_or_string/1" do
+ test "describes regex" do
+ assert "~r/foo/i" == Utils.describe_regex_or_string(~r/foo/i)
+ end
+
+ test "returns string as-is" do
+ assert "foo" == Utils.describe_regex_or_string("foo")
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c7a62be18..4703c3801 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -116,4 +116,53 @@ test "a Note without replies/first/items validates" do
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+
+ test "Fedibird quote post" do
+ insert(:user, ap_id: "https://fedibird.com/users/noellabo")
+
+ data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
+ end
+
+ test "Fedibird quote post with quoteUri field" do
+ insert(:user, ap_id: "https://fedibird.com/users/noellabo")
+
+ data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729"
+ end
+
+ test "Misskey quote post" do
+ insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
+ end
+
+ test "Parse tag as quote" do
+ # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
+
+ insert(:user, ap_id: "https://server.example/users/1")
+
+ data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://server.example/objects/123"
+
+ assert Enum.at(cng.changes.tag, 0).changes == %{
+ type: "Link",
+ mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ href: "https://server.example/objects/123",
+ name: "RE: https://server.example/objects/123"
+ }
+ end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs
index ea4664859..bbb31516c 100644
--- a/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do
- use Pleroma.DataCase, async: true
+ use Pleroma.DataCase, async: false
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
@@ -90,17 +90,26 @@ test "it's invalid if the actor of the object and the actor of delete are from d
assert {:actor, {"is not allowed to modify object", []}} in cng.errors
end
- test "it's valid if the actor of the object is a local superuser",
+ test "it's only valid if the actor of the object is a privileged local user",
%{valid_post_delete: valid_post_delete} do
+ clear_config([:instance, :moderator_privileges], [:messages_delete])
+
user =
insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
- valid_other_actor =
+ post_delete_with_moderator_actor =
valid_post_delete
|> Map.put("actor", user.ap_id)
- {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
+ {:ok, _, meta} = ObjectValidator.validate(post_delete_with_moderator_actor, [])
+
assert meta[:do_not_federate]
+
+ clear_config([:instance, :moderator_privileges], [])
+
+ {:error, cng} = ObjectValidator.validate(post_delete_with_moderator_actor, [])
+
+ assert {:actor, {"is not allowed to modify object", []}} in cng.errors
end
end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs
index bbdb09c4c..9bb291a38 100644
--- a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs
@@ -38,16 +38,70 @@ test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emo
assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
end
- test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
+ test "it is valid when custom emoji is used", %{valid_emoji_react: valid_emoji_react} do
without_emoji_content =
valid_emoji_react
- |> Map.put("content", "x")
+ |> Map.put("content", ":hello:")
+ |> Map.put("tag", [
+ %{
+ "type" => "Emoji",
+ "name" => ":hello:",
+ "icon" => %{"url" => "http://somewhere", "type" => "Image"}
+ }
+ ])
+
+ {:ok, _, _} = ObjectValidator.validate(without_emoji_content, [])
+ end
+
+ test "it is not valid when custom emoji don't have a matching tag", %{
+ valid_emoji_react: valid_emoji_react
+ } do
+ without_emoji_content =
+ valid_emoji_react
+ |> Map.put("content", ":hello:")
+ |> Map.put("tag", [
+ %{
+ "type" => "Emoji",
+ "name" => ":whoops:",
+ "icon" => %{"url" => "http://somewhere", "type" => "Image"}
+ }
+ ])
{:error, cng} = ObjectValidator.validate(without_emoji_content, [])
refute cng.valid?
- assert {:content, {"must be a single character emoji", []}} in cng.errors
+ assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
+ end
+
+ test "it is not valid when custom emoji have no tags", %{
+ valid_emoji_react: valid_emoji_react
+ } do
+ without_emoji_content =
+ valid_emoji_react
+ |> Map.put("content", ":hello:")
+ |> Map.put("tag", [])
+
+ {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+ refute cng.valid?
+
+ assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
+ end
+
+ test "it is not valid when custom emoji doesn't match a shortcode format", %{
+ valid_emoji_react: valid_emoji_react
+ } do
+ without_emoji_content =
+ valid_emoji_react
+ |> Map.put("content", "hello")
+ |> Map.put("tag", [])
+
+ {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+ refute cng.valid?
+
+ assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
end
end
end
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index e2db3d575..c5137cbb7 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -276,8 +276,7 @@ test "publish to url with with different ports" do
follower =
insert(:user, %{
local: false,
- inbox: "https://domain.com/users/nick1/inbox",
- ap_enabled: true
+ inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
@@ -313,8 +312,7 @@ test "publish to url with with different ports" do
follower =
insert(:user, %{
local: false,
- inbox: "https://domain.com/users/nick1/inbox",
- ap_enabled: true
+ inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
@@ -348,8 +346,7 @@ test "publish to url with with different ports" do
follower =
insert(:user, %{
local: false,
- inbox: "https://domain.com/users/nick1/inbox",
- ap_enabled: true
+ inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
@@ -382,15 +379,13 @@ test "publish to url with with different ports" do
fetcher =
insert(:user,
local: false,
- inbox: "https://domain.com/users/nick1/inbox",
- ap_enabled: true
+ inbox: "https://domain.com/users/nick1/inbox"
)
another_fetcher =
insert(:user,
local: false,
- inbox: "https://domain2.com/users/nick1/inbox",
- ap_enabled: true
+ inbox: "https://domain2.com/users/nick1/inbox"
)
actor = insert(:user)
diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs
index b24831e85..6820e23d0 100644
--- a/test/pleroma/web/activity_pub/side_effects_test.exs
+++ b/test/pleroma/web/activity_pub/side_effects_test.exs
@@ -453,7 +453,7 @@ test "adds the reaction to the object", %{emoji_react: emoji_react, user: user}
object = Object.get_by_ap_id(emoji_react.data["object"])
assert object.data["reaction_count"] == 1
- assert ["👌", [user.ap_id]] in object.data["reactions"]
+ assert ["👌", [user.ap_id], nil] in object.data["reactions"]
end
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs
index 9d99df27c..f2e1cefa3 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs
@@ -34,7 +34,56 @@ test "it works for incoming emoji reactions" do
object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1
- assert match?([["👌", _]], object.data["reactions"])
+ assert match?([["👌", _, nil]], object.data["reactions"])
+ end
+
+ test "it works for incoming custom emoji reactions" do
+ user = insert(:user)
+ other_user = insert(:user, local: false)
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+ data =
+ File.read!("test/fixtures/custom-emoji-reaction.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("actor", other_user.ap_id)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == other_user.ap_id
+ assert data["type"] == "EmojiReact"
+ assert data["id"] == "https://misskey.local.live/likes/917ocsybgp"
+ assert data["object"] == activity.data["object"]
+ assert data["content"] == ":hanapog:"
+
+ assert data["tag"] == [
+ %{
+ "id" => "https://misskey.local.live/emojis/hanapog",
+ "type" => "Emoji",
+ "name" => "hanapog",
+ "updated" => "2022-06-07T12:00:05.773Z",
+ "icon" => %{
+ "type" => "Image",
+ "url" =>
+ "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
+ }
+ }
+ ]
+
+ object = Object.get_by_ap_id(data["object"])
+
+ assert object.data["reaction_count"] == 1
+
+ assert match?(
+ [
+ [
+ "hanapog",
+ _,
+ "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
+ ]
+ ],
+ object.data["reactions"]
+ )
end
test "it works for incoming unqualified emoji reactions" do
@@ -65,7 +114,7 @@ test "it works for incoming unqualified emoji reactions" do
object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1
- assert match?([[emoji, _]], object.data["reactions"])
+ assert match?([[^emoji, _, _]], object.data["reactions"])
end
test "it reject invalid emoji reactions" do
diff --git a/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs
new file mode 100644
index 000000000..b85f0a477
--- /dev/null
+++ b/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs
@@ -0,0 +1,50 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.ImageHandlingTest do
+ use Oban.Testing, repo: Pleroma.Repo
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ test "Hubzilla Image object" do
+ Tesla.Mock.mock(fn
+ %{url: "https://hub.somaton.com/channel/testc6"} ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/hubzilla-actor.json"),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+ end)
+
+ data = File.read!("test/fixtures/hubzilla-create-image.json") |> Poison.decode!()
+
+ {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ assert object = Object.normalize(activity, fetch: false)
+
+ assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
+
+ assert object.data["cc"] == ["https://hub.somaton.com/followers/testc6"]
+
+ assert object.data["attachment"] == [
+ %{
+ "mediaType" => "image/jpeg",
+ "type" => "Link",
+ "url" => [
+ %{
+ "height" => 2200,
+ "href" =>
+ "https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-0.jpg",
+ "mediaType" => "image/jpeg",
+ "type" => "Link",
+ "width" => 2200
+ }
+ ]
+ }
+ ]
+ end
+end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index 7c406fbd0..a9ad3e9c8 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -104,6 +104,7 @@ test "it does not fetch reply-to activities beyond max replies depth limit" do
end
end
+ @tag capture_log: true
test "it does not crash if the object in inReplyTo can't be fetched" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@@ -723,6 +724,7 @@ test "the standalone note uses its own ID when context is missing" do
assert modified.data["context"] == object.data["id"]
end
+ @tag capture_log: true
test "the reply note uses its parent's ID when context is missing and reply is unreachable" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 6b4636d22..5e58d75db 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity
alias Pleroma.Object
- alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
@@ -123,6 +122,39 @@ test "it fixes both the Create and object contexts in a reply" do
assert activity.data["context"] == object.data["context"]
end
+
+ test "it keeps link tags" do
+ insert(:user, ap_id: "https://example.org/users/alice")
+
+ message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!()
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ object = Object.normalize(activity)
+ assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"]
+ end
+
+ test "it accepts quote posts" do
+ insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Create",
+ "actor" => "https://misskey.io/users/7rkrarq81i",
+ "object" => object
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ # Object was created in the database
+ object = Object.normalize(activity)
+ assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0"
+
+ # It fetched the quoted post
+ assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
+ end
end
describe "prepare outgoing" do
@@ -337,68 +369,19 @@ test "Updates of Notes are handled" do
}
} = prepared["object"]
end
- end
- describe "user upgrade" do
- test "it upgrades a user to activitypub" do
- user =
- insert(:user, %{
- nickname: "rye@niu.moe",
- local: false,
- ap_id: "https://niu.moe/users/rye",
- follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
- })
+ test "it prepares a quote post" do
+ user = insert(:user)
- user_two = insert(:user)
- Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
+ {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id})
- {:ok, activity} = CommonAPI.post(user, %{status: "test"})
- {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"})
- assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
+ {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
- user = User.get_cached_by_id(user.id)
- assert user.note_count == 1
+ %{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
- {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
- ObanHelpers.perform_all()
-
- assert user.ap_enabled
- assert user.note_count == 1
- assert user.follower_address == "https://niu.moe/users/rye/followers"
- assert user.following_address == "https://niu.moe/users/rye/following"
-
- user = User.get_cached_by_id(user.id)
- assert user.note_count == 1
-
- activity = Activity.get_by_id(activity.id)
- assert user.follower_address in activity.recipients
-
- assert %{
- "url" => [
- %{
- "href" =>
- "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
- }
- ]
- } = user.avatar
-
- assert %{
- "url" => [
- %{
- "href" =>
- "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
- }
- ]
- } = user.banner
-
- refute "..." in activity.recipients
-
- unrelated_activity = Activity.get_by_id(unrelated_activity.id)
- refute user.follower_address in unrelated_activity.recipients
-
- user_two = User.get_cached_by_id(user_two.id)
- assert User.following?(user_two, user)
- refute "..." in User.following(user_two)
+ assert modified["object"]["quoteUrl"] == quote_id
+ assert modified["object"]["quoteUri"] == quote_id
end
end
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index e7d1e01c4..3f93c872b 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -587,15 +587,38 @@ test "removes actor from announcements" do
end
describe "get_cached_emoji_reactions/1" do
- test "returns the data or an emtpy list" do
+ test "returns the normalized data or an emtpy list" do
object = insert(:note)
assert Utils.get_cached_emoji_reactions(object) == []
object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]})
- assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]]
+ assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"], nil]]
object = insert(:note, data: %{"reactions" => %{}})
assert Utils.get_cached_emoji_reactions(object) == []
end
end
+
+ describe "add_emoji_reaction_to_object/1" do
+ test "works with legacy 2-tuple format" do
+ user = insert(:user)
+ other_user = insert(:user)
+ third_user = insert(:user)
+
+ note =
+ insert(:note,
+ user: user,
+ data: %{
+ "reactions" => [["😿", [other_user.ap_id]]]
+ }
+ )
+
+ _activity = insert(:note_activity, user: user, note: note)
+
+ Utils.add_emoji_reaction_to_object(
+ %Activity{data: %{"content" => "😿", "actor" => third_user.ap_id}},
+ note
+ )
+ end
+ end
end
diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
index d83f7f011..e1ab50542 100644
--- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import ExUnit.CaptureLog
@@ -92,18 +92,12 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro
describe "PUT /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_tags])
+
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
- conn =
- conn
- |> put_req_header("accept", "application/json")
- |> put(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
- "#{user2.nickname}&tags[]=foo&tags[]=bar"
- )
-
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
@@ -113,6 +107,14 @@ test "it appends specified tags to users with specified nicknames", %{
user1: user1,
user2: user2
} do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> put(
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=foo&tags[]=bar"
+ )
+
assert empty_json_response(conn)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
@@ -130,26 +132,43 @@ test "it appends specified tags to users with specified nicknames", %{
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
- test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
+ test "it does not modify tags of not specified users", %{
+ conn: conn,
+ user1: user1,
+ user2: user2,
+ user3: user3
+ } do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> put(
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=foo&tags[]=bar"
+ )
+
assert empty_json_response(conn)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
+
+ test "it requires privileged role :users_manage_tags", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> put("/api/pleroma/admin/users/tag?nicknames[]=nickname&tags[]=foo&tags[]=bar")
+
+ assert json_response(response, :forbidden)
+ end
end
describe "DELETE /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_tags])
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
- conn =
- conn
- |> put_req_header("accept", "application/json")
- |> delete(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
- "#{user2.nickname}&tags[]=x&tags[]=z"
- )
-
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
@@ -159,6 +178,14 @@ test "it removes specified tags from users with specified nicknames", %{
user1: user1,
user2: user2
} do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete(
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=x&tags[]=z"
+ )
+
assert empty_json_response(conn)
assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"]
@@ -176,10 +203,34 @@ test "it removes specified tags from users with specified nicknames", %{
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
- test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
+ test "it does not modify tags of not specified users", %{
+ conn: conn,
+ user1: user1,
+ user2: user2,
+ user3: user3
+ } do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete(
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=x&tags[]=z"
+ )
+
assert empty_json_response(conn)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
+
+ test "it requires privileged role :users_manage_tags", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users/tag?nicknames[]=nickname&tags[]=foo&tags[]=bar")
+
+ assert json_response(response, :forbidden)
+ end
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
@@ -271,21 +322,38 @@ test "/:right DELETE, can remove from a permission group (multiple)", %{
end
end
- test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
- user = insert(:user)
+ describe "/api/pleroma/admin/users/:nickname/password_reset" do
+ test "it returns a password reset link", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
- conn =
- conn
- |> put_req_header("accept", "application/json")
- |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
+ user = insert(:user)
- resp = json_response(conn, 200)
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
- assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
+ resp = json_response(conn, 200)
+
+ assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
+ end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/api/pleroma/admin/users/nickname/password_reset")
+
+ assert json_response(response, :forbidden)
+ end
end
describe "PUT disable_mfa" do
test "returns 200 and disable 2fa", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
+
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
@@ -307,6 +375,8 @@ test "returns 200 and disable 2fa", %{conn: conn} do
end
test "returns 404 if user not found", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
+
response =
conn
|> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
@@ -314,6 +384,16 @@ test "returns 404 if user not found", %{conn: conn} do
assert response == %{"error" => "Not found"}
end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
+
+ assert json_response(response, :forbidden)
+ end
end
describe "GET /api/pleroma/admin/restart" do
@@ -344,6 +424,8 @@ test "need_reboot flag", %{conn: conn} do
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+
user = insert(:user)
insert(:note_activity, user: user)
@@ -360,6 +442,14 @@ test "renders user's statuses", %{conn: conn, user: user} do
assert length(activities) == 3
end
+ test "it requires privileged role :messages_read", %{conn: conn, user: user} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "renders user's statuses with pagination", %{conn: conn, user: user} do
%{"total" => 3, "activities" => [activity1]} =
conn
@@ -421,21 +511,32 @@ test "excludes reblogs by default", %{conn: conn, user: user} do
describe "GET /api/pleroma/admin/users/:nickname/chats" do
setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+
user = insert(:user)
+
+ %{user: user}
+ end
+
+ test "renders user's chats", %{conn: conn, user: user} do
recipients = insert_list(3, :user)
Enum.each(recipients, fn recipient ->
CommonAPI.post_chat_message(user, recipient, "yo")
end)
- %{user: user}
- end
-
- test "renders user's chats", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats")
assert json_response(conn, 200) |> length() == 3
end
+
+ test "it requires privileged role :messages_read", %{conn: conn, user: user} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats")
+
+ assert json_response(conn, :forbidden)
+ end
end
describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do
@@ -471,6 +572,7 @@ test "returns 403", %{conn: conn, user: user} do
describe "GET /api/pleroma/admin/moderation_log" do
setup do
+ clear_config([:instance, :admin_privileges], [:moderation_log_read])
moderator = insert(:user, is_moderator: true)
%{moderator: moderator}
@@ -675,6 +777,15 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do
assert get_in(first_entry, ["data", "message"]) ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
+
+ test "it requires privileged role :moderation_log_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> get("/api/pleroma/admin/moderation_log")
+ |> json_response(:forbidden)
+ end
end
test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated",
@@ -688,6 +799,7 @@ test "gets a remote users when [:instance, :limit_to_local_content] is set to :u
describe "GET /users/:nickname/credentials" do
test "gets the user credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials")
@@ -696,6 +808,7 @@ test "gets the user credentials", %{conn: conn} do
end
test "returns 403 if requested by a non-admin" do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
user = insert(:user)
conn =
@@ -705,6 +818,16 @@ test "returns 403 if requested by a non-admin" do
assert json_response(conn, :forbidden)
end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/users/nickname/credentials")
+
+ assert json_response(response, :forbidden)
+ end
end
describe "PATCH /users/:nickname/credentials" do
@@ -714,6 +837,8 @@ test "returns 403 if requested by a non-admin" do
end
test "changes password and email", %{conn: conn, admin: admin, user: user} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
+
assert user.password_reset_pending == false
conn =
@@ -756,6 +881,19 @@ test "returns 403 if requested by a non-admin", %{user: user} do
assert json_response(conn, :forbidden)
end
+ test "returns 403 if not privileged with :users_manage_credentials", %{conn: conn, user: user} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
+ "password" => "new_password",
+ "email" => "new_email@example.com",
+ "name" => "new_name"
+ })
+
+ assert json_response(conn, :forbidden)
+ end
+
test "changes actor type from permitted list", %{conn: conn, user: user} do
assert user.actor_type == "Person"
@@ -784,6 +922,7 @@ test "update non existing user", %{conn: conn} do
describe "PATCH /users/:nickname/force_password_reset" do
test "sets password_reset_pending to true", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
user = insert(:user)
assert user.password_reset_pending == false
@@ -796,10 +935,21 @@ test "sets password_reset_pending to true", %{conn: conn} do
assert User.get_by_id(user.id).password_reset_pending == true
end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> patch("/api/pleroma/admin/users/force_password_reset", %{nickname: "nickname"})
+
+ assert json_response(response, :forbidden)
+ end
end
describe "PATCH /confirm_email" do
test "it confirms emails of two users", %{conn: conn, admin: admin} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
[first_user, second_user] = insert_pair(:user, is_confirmed: false)
refute first_user.is_confirmed
@@ -826,10 +976,21 @@ test "it confirms emails of two users", %{conn: conn, admin: admin} do
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{second_user.nickname}"
end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> patch("/api/pleroma/admin/users/confirm_email", %{nicknames: ["nickname"]})
+
+ assert json_response(response, :forbidden)
+ end
end
describe "PATCH /resend_confirmation_email" do
test "it resend emails for two users", %{conn: conn, admin: admin} do
+ clear_config([:instance, :admin_privileges], [:users_manage_credentials])
[first_user, second_user] = insert_pair(:user, is_confirmed: false)
ret_conn =
@@ -855,9 +1016,23 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do
|> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
|> assert_email_sent()
end
+
+ test "it requires privileged role :users_manage_credentials", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{nicknames: ["nickname"]})
+
+ assert json_response(response, :forbidden)
+ end
end
describe "/api/pleroma/admin/stats" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:statistics_read])
+ end
+
test "status visibility count", %{conn: conn} do
user = insert(:user)
CommonAPI.post(user, %{visibility: "public", status: "hey"})
@@ -890,6 +1065,14 @@ test "by instance", %{conn: conn} do
assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} =
response["status_visibility"]
end
+
+ test "it requires privileged role :statistics_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> get("/api/pleroma/admin/stats", instance: "lain.wired")
+ |> json_response(:forbidden)
+ end
end
describe "/api/pleroma/backups" do
@@ -958,6 +1141,34 @@ test "it doesn't limit admins", %{conn: conn} do
assert Repo.aggregate(Pleroma.User.Backup, :count) == 2
end
end
+
+ describe "POST /api/v1/pleroma/admin/reload_emoji" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
+
+ admin = insert(:user, is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+
+ {:ok, %{conn: conn, admin: admin}}
+ end
+
+ test "it requires privileged role :emoji_manage_emoji", %{conn: conn} do
+ assert conn
+ |> post("/api/v1/pleroma/admin/reload_emoji")
+ |> json_response(200)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> post("/api/v1/pleroma/admin/reload_emoji")
+ |> json_response(:forbidden)
+ end
+ end
end
# Needed for testing
diff --git a/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs b/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs
index 5b8148c05..cf60bcad5 100644
--- a/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs
@@ -3,11 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
setup do
+ clear_config([:instance, :admin_privileges], [:announcements_manage_announcements])
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
@@ -31,6 +32,18 @@ test "it lists all announcements", %{conn: conn} do
assert [%{"id" => ^id}] = response
end
+ test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do
+ conn
+ |> get("/api/v1/pleroma/admin/announcements")
+ |> json_response_and_validate_schema(:ok)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> get("/api/v1/pleroma/admin/announcements")
+ |> json_response(:forbidden)
+ end
+
test "it paginates announcements", %{conn: conn} do
_announcements = Enum.map(0..20, fn _ -> insert(:announcement) end)
@@ -92,6 +105,20 @@ test "it displays one announcement", %{conn: conn} do
assert %{"id" => ^id} = response
end
+ test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do
+ %{id: id} = insert(:announcement)
+
+ conn
+ |> get("/api/v1/pleroma/admin/announcements/#{id}")
+ |> json_response_and_validate_schema(:ok)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> get("/api/v1/pleroma/admin/announcements/#{id}")
+ |> json_response(:forbidden)
+ end
+
test "it returns not found for non-existent id", %{conn: conn} do
%{id: id} = insert(:announcement)
@@ -112,6 +139,20 @@ test "it deletes specified announcement", %{conn: conn} do
|> json_response_and_validate_schema(:ok)
end
+ test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do
+ %{id: id} = insert(:announcement)
+
+ conn
+ |> delete("/api/v1/pleroma/admin/announcements/#{id}")
+ |> json_response_and_validate_schema(:ok)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> delete("/api/v1/pleroma/admin/announcements/#{id}")
+ |> json_response(:forbidden)
+ end
+
test "it returns not found for non-existent id", %{conn: conn} do
%{id: id} = insert(:announcement)
@@ -156,6 +197,29 @@ test "it updates a field", %{conn: conn} do
assert NaiveDateTime.compare(new.starts_at, starts_at) == :eq
end
+ test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do
+ %{id: id} = insert(:announcement)
+
+ now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+ starts_at = NaiveDateTime.add(now, -10, :second)
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/v1/pleroma/admin/announcements/#{id}", %{
+ starts_at: NaiveDateTime.to_iso8601(starts_at)
+ })
+ |> json_response_and_validate_schema(:ok)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/v1/pleroma/admin/announcements/#{id}", %{
+ starts_at: NaiveDateTime.to_iso8601(starts_at)
+ })
+ |> json_response(:forbidden)
+ end
+
test "it updates with time with utc timezone", %{conn: conn} do
%{id: id} = insert(:announcement)
@@ -250,6 +314,36 @@ test "it creates an announcement", %{conn: conn} do
assert NaiveDateTime.compare(announcement.ends_at, ends_at) == :eq
end
+ test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do
+ content = "test post announcement api"
+
+ now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+ starts_at = NaiveDateTime.add(now, -10, :second)
+ ends_at = NaiveDateTime.add(now, 10, :second)
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/admin/announcements", %{
+ "content" => content,
+ "starts_at" => NaiveDateTime.to_iso8601(starts_at),
+ "ends_at" => NaiveDateTime.to_iso8601(ends_at),
+ "all_day" => true
+ })
+ |> json_response_and_validate_schema(:ok)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/admin/announcements", %{
+ "content" => content,
+ "starts_at" => NaiveDateTime.to_iso8601(starts_at),
+ "ends_at" => NaiveDateTime.to_iso8601(ends_at),
+ "all_day" => true
+ })
+ |> json_response(:forbidden)
+ end
+
test "creating with time with utc timezones", %{conn: conn} do
content = "test post announcement api"
diff --git a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs
index 0ef7c367b..1b5c31b7d 100644
--- a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatControllerTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -27,7 +27,10 @@ defp admin_setup do
end
describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do
- setup do: admin_setup()
+ setup do
+ clear_config([:instance, :admin_privileges], [:messages_delete])
+ admin_setup()
+ end
test "it deletes a message from the chat", %{conn: conn, admin: admin} do
user = insert(:user)
@@ -60,10 +63,22 @@ test "it deletes a message from the chat", %{conn: conn, admin: admin} do
refute MessageReference.get_by_id(recipient_cm_ref.id)
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
end
+
+ test "it requires privileged role :messages_delete", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/admin/chats/some_id/messages/some_ref_id")
+ |> json_response(:forbidden)
+ end
end
describe "GET /api/pleroma/admin/chats/:id/messages" do
- setup do: admin_setup()
+ setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+ admin_setup()
+ end
test "it paginates", %{conn: conn} do
user = insert(:user)
@@ -114,10 +129,21 @@ test "it returns the messages for a given chat", %{conn: conn} do
assert length(result) == 3
end
+
+ test "it requires privileged role :messages_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/chats/some_id/messages")
+
+ assert json_response(conn, :forbidden)
+ end
end
describe "GET /api/pleroma/admin/chats/:id" do
- setup do: admin_setup()
+ setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+ admin_setup()
+ end
test "it returns a chat", %{conn: conn} do
user = insert(:user)
@@ -135,6 +161,14 @@ test "it returns a chat", %{conn: conn} do
assert %{} = result["receiver"]
refute result["account"]
end
+
+ test "it requires privileged role :messages_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/chats/some_id")
+
+ assert json_response(conn, :forbidden)
+ end
end
describe "unauthorized chat moderation" do
diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
index 9ef7c0c46..19ce3681c 100644
--- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
@@ -316,6 +316,7 @@ test "create new config setting in db", %{conn: conn} do
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
+ @tag capture_log: true
test "save configs setting without explicit key", %{conn: conn} do
adapter = Application.get_env(:http, :adapter)
send_user_agent = Application.get_env(:http, :send_user_agent)
@@ -1501,15 +1502,14 @@ test "filters by database configuration whitelist", %{conn: conn} do
clear_config(:database_config_whitelist, [
{:pleroma, :instance},
{:pleroma, :activitypub},
- {:pleroma, Pleroma.Upload},
- {:esshd}
+ {:pleroma, Pleroma.Upload}
])
conn = get(conn, "/api/pleroma/admin/config/descriptions")
children = json_response_and_validate_schema(conn, 200)
- assert length(children) == 4
+ assert length(children) == 3
assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3
@@ -1521,9 +1521,6 @@ test "filters by database configuration whitelist", %{conn: conn} do
web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end)
assert web_endpoint["children"]
-
- esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end)
- assert esshd["children"]
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
index 38a23b224..0d1a4999e 100644
--- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
@@ -89,6 +89,7 @@ test "from available frontends", %{conn: conn} do
"build_url" => "http://gensokyo.2hu/builds/${ref}",
"git" => nil,
"installed" => true,
+ "installed_refs" => ["fantasy"],
"name" => "pleroma",
"ref" => "fantasy"
}
diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
index 72436cd83..6cca623f3 100644
--- a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
@@ -31,6 +31,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
end
test "GET /instances/:instance/statuses", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:messages_read])
user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme")
user2 = insert(:user, local: false, ap_id: "https://test.com/users/test")
insert_pair(:note_activity, user: user)
@@ -60,9 +61,14 @@ test "GET /instances/:instance/statuses", %{conn: conn} do
|> json_response(200)
assert length(activities) == 3
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(:forbidden)
end
test "DELETE /instances/:instance", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [:instances_delete])
user = insert(:user, nickname: "lain@lain.com")
post = insert(:note_activity, user: user)
@@ -76,5 +82,11 @@ test "DELETE /instances/:instance", %{conn: conn} do
assert response == "lain.com"
refute Repo.reload(user).is_active
refute Repo.reload(post)
+
+ clear_config([:instance, :admin_privileges], [])
+
+ conn
+ |> delete("/api/pleroma/admin/instances/lain.com")
+ |> json_response(:forbidden)
end
end
diff --git a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
index b9d48a4b6..8051cb2e9 100644
--- a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InviteControllerTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -23,8 +23,25 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do
end
describe "POST /api/pleroma/admin/users/email_invite, with valid config" do
- setup do: clear_config([:instance, :registrations_open], false)
- setup do: clear_config([:instance, :invites_enabled], true)
+ setup do
+ clear_config([:instance, :registrations_open], false)
+ clear_config([:instance, :invites_enabled], true)
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+ end
+
+ test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json;charset=utf-8")
+ |> post("/api/pleroma/admin/users/email_invite", %{
+ email: "foo@bar.com",
+ name: "J. D."
+ })
+
+ assert json_response(conn, :forbidden)
+ end
test "sends invitation and returns 204", %{admin: admin, conn: conn} do
recipient_email = "foo@bar.com"
@@ -114,8 +131,11 @@ test "email with +", %{conn: conn, admin: admin} do
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
- setup do: clear_config([:instance, :registrations_open])
- setup do: clear_config([:instance, :invites_enabled])
+ setup do
+ clear_config([:instance, :registrations_open])
+ clear_config([:instance, :invites_enabled])
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+ end
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
clear_config([:instance, :registrations_open], false)
@@ -157,6 +177,21 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
end
describe "POST /api/pleroma/admin/users/invite_token" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+ end
+
+ test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/users/invite_token")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "without options", %{conn: conn} do
conn =
conn
@@ -221,6 +256,18 @@ test "with max use and expires_at", %{conn: conn} do
end
describe "GET /api/pleroma/admin/users/invites" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+ end
+
+ test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/users/invites")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
@@ -249,6 +296,21 @@ test "with invite", %{conn: conn} do
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+ end
+
+ test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
+
+ assert json_response(conn, :forbidden)
+ end
+
test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs
index 6fd3fbe5a..fb2579a3d 100644
--- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -26,6 +26,20 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
end
describe "GET /api/pleroma/admin/reports/:id" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:reports_manage_reports])
+ end
+
+ test "returns 403 if not privileged with :reports_manage_reports", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> get("/api/pleroma/admin/reports/report_id")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -89,6 +103,8 @@ test "returns 404 when report id is invalid", %{conn: conn} do
describe "PATCH /api/pleroma/admin/reports" do
setup do
+ clear_config([:instance, :admin_privileges], [:reports_manage_reports])
+
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -107,11 +123,30 @@ test "returns 404 when report id is invalid", %{conn: conn} do
})
%{
+ reporter: reporter,
id: report_id,
second_report_id: second_report_id
}
end
+ test "returns 403 if not privileged with :reports_manage_reports", %{
+ conn: conn,
+ id: id,
+ admin: admin
+ } do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> assign(:token, insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]))
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [%{"state" => "resolved", "id" => id}]
+ })
+
+ assert json_response(conn, :forbidden)
+ end
+
test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do
read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"])
write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"])
@@ -232,9 +267,43 @@ test "updates state of multiple reports", %{
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} (on user @#{second_activity.user_actor.nickname}) with 'closed' state"
end
+
+ test "works if reporter is deactivated", %{
+ conn: conn,
+ id: id,
+ reporter: reporter
+ } do
+ Pleroma.User.set_activation(reporter, false)
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "resolved", "id" => id}
+ ]
+ })
+ |> json_response_and_validate_schema(:no_content)
+
+ activity = Activity.get_by_id_with_user_actor(id)
+ assert activity.data["state"] == "resolved"
+ end
end
describe "GET /api/pleroma/admin/reports" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:reports_manage_reports])
+ end
+
+ test "returns 403 if not privileged with :reports_manage_reports", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> get(report_path(conn, :index))
+
+ assert json_response(conn, :forbidden)
+ end
+
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
@@ -318,6 +387,34 @@ test "returns reports with specified state", %{conn: conn} do
|> json_response_and_validate_schema(:ok)
end
+ test "renders content correctly", %{conn: conn} do
+ [reporter, target_user] = insert_pair(:user)
+ note = insert(:note, user: target_user, data: %{"content" => "mew 1"})
+ note2 = insert(:note, user: target_user, data: %{"content" => "mew 2"})
+ activity = insert(:note_activity, user: target_user, note: note)
+ activity2 = insert(:note_activity, user: target_user, note: note2)
+
+ {:ok, _report} =
+ CommonAPI.report(reporter, %{
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id, activity2.id]
+ })
+
+ CommonAPI.delete(activity.id, target_user)
+ CommonAPI.delete(activity2.id, target_user)
+
+ response =
+ conn
+ |> get(report_path(conn, :index))
+ |> json_response_and_validate_schema(:ok)
+
+ assert [open_report] = response["reports"]
+ assert %{"statuses" => [s1, s2]} = open_report
+ assert "mew 1" in [s1["content"], s2["content"]]
+ assert "mew 2" in [s1["content"], s2["content"]]
+ end
+
test "returns 403 when requested by a non-admin" do
user = insert(:user)
token = insert(:oauth_token, user: user)
@@ -343,6 +440,8 @@ test "returns 403 when requested by anonymous" do
describe "POST /api/pleroma/admin/reports/:id/notes" do
setup %{conn: conn, admin: admin} do
+ clear_config([:instance, :admin_privileges], [:reports_manage_reports])
+
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -371,6 +470,25 @@ test "returns 403 when requested by anonymous" do
}
end
+ test "returns 403 if not privileged with :reports_manage_reports", %{
+ conn: conn,
+ report_id: report_id
+ } do
+ clear_config([:instance, :admin_privileges], [])
+
+ post_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+ content: "this is disgusting2!"
+ })
+
+ delete_conn = delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/note.id")
+
+ assert json_response(post_conn, :forbidden)
+ assert json_response(delete_conn, :forbidden)
+ end
+
test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
assert [note, _] = Repo.all(ReportNote)
diff --git a/test/pleroma/web/admin_api/controllers/status_controller_test.exs b/test/pleroma/web/admin_api/controllers/status_controller_test.exs
index 8bb96ca87..8908a2812 100644
--- a/test/pleroma/web/admin_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/status_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusControllerTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -26,6 +26,10 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do
end
describe "GET /api/pleroma/admin/statuses/:id" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+ end
+
test "not found", %{conn: conn} do
assert conn
|> get("/api/pleroma/admin/statuses/not_found")
@@ -50,10 +54,17 @@ test "shows activity", %{conn: conn} do
assert account["is_active"] == actor.is_active
assert account["is_confirmed"] == actor.is_confirmed
end
+
+ test "denies reading activity when not privileged", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn |> get("/api/pleroma/admin/statuses/some_id") |> json_response(:forbidden)
+ end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
setup do
+ clear_config([:instance, :admin_privileges], [:messages_delete])
activity = insert(:note_activity)
%{id: activity.id}
@@ -122,10 +133,20 @@ test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
assert %{"error" => "test - Invalid value for enum."} =
json_response_and_validate_schema(conn, :bad_request)
end
+
+ test "it requires privileged role :messages_delete", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/pleroma/admin/statuses/some_id", %{})
+ |> json_response(:forbidden)
+ end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
setup do
+ clear_config([:instance, :admin_privileges], [:messages_delete])
activity = insert(:note_activity)
%{id: activity.id}
@@ -149,9 +170,22 @@ test "returns 404 when the status does not exist", %{conn: conn} do
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"}
end
+
+ test "it requires privileged role :messages_delete", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/admin/statuses/some_id")
+ |> json_response(:forbidden)
+ end
end
describe "GET /api/pleroma/admin/statuses" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:messages_read])
+ end
+
test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do
blocked = insert(:user)
user = insert(:user)
@@ -197,5 +231,13 @@ test "returns private and direct statuses with godmode on", %{conn: conn, admin:
conn = get(conn, "/api/pleroma/admin/statuses?godmode=true")
assert json_response_and_validate_schema(conn, 200) |> length() == 3
end
+
+ test "it requires privileged role :messages_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/statuses")
+
+ assert json_response(conn, :forbidden)
+ end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
index 79971be06..bb9dcb4aa 100644
--- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.UserControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Mock
@@ -38,6 +38,7 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
end
test "with valid `admin_token` query parameter, skips OAuth scopes check" do
+ clear_config([:instance, :admin_privileges], [:users_read])
clear_config([:admin_token], "password123")
user = insert(:user)
@@ -47,53 +48,10 @@ test "with valid `admin_token` query parameter, skips OAuth scopes check" do
assert json_response_and_validate_schema(conn, 200)
end
- test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
- %{admin: admin} do
- user = insert(:user)
- url = "/api/pleroma/admin/users/#{user.nickname}"
-
- good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
- good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
- good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
-
- bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
- bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
- bad_token3 = nil
-
- for good_token <- [good_token1, good_token2, good_token3] do
- conn =
- build_conn()
- |> assign(:user, admin)
- |> assign(:token, good_token)
- |> get(url)
-
- assert json_response_and_validate_schema(conn, 200)
- end
-
- for good_token <- [good_token1, good_token2, good_token3] do
- conn =
- build_conn()
- |> assign(:user, nil)
- |> assign(:token, good_token)
- |> get(url)
-
- assert json_response(conn, :forbidden)
- end
-
- for bad_token <- [bad_token1, bad_token2, bad_token3] do
- conn =
- build_conn()
- |> assign(:user, admin)
- |> assign(:token, bad_token)
- |> get(url)
-
- assert json_response_and_validate_schema(conn, :forbidden)
- end
- end
-
describe "DELETE /api/pleroma/admin/users" do
test "single user", %{admin: admin, conn: conn} do
clear_config([:instance, :federating], true)
+ clear_config([:instance, :admin_privileges], [:users_delete])
user =
insert(:user,
@@ -149,6 +107,8 @@ test "single user", %{admin: admin, conn: conn} do
end
test "multiple users", %{admin: admin, conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_delete])
+
user_one = insert(:user)
user_two = insert(:user)
@@ -168,6 +128,17 @@ test "multiple users", %{admin: admin, conn: conn} do
assert response -- [user_one.nickname, user_two.nickname] == []
end
+
+ test "Needs privileged role", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users?nickname=nickname")
+
+ assert json_response(response, :forbidden)
+ end
end
describe "/api/pleroma/admin/users" do
@@ -307,7 +278,19 @@ test "Multiple user creation works in transaction", %{conn: conn} do
end
end
- describe "/api/pleroma/admin/users/:nickname" do
+ describe "GET /api/pleroma/admin/users/:nickname" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:users_read])
+ end
+
+ test "returns 403 if not privileged with :users_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/users/user.nickname")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "Show", %{conn: conn} do
user = insert(:user)
@@ -323,6 +306,50 @@ test "when the user doesn't exist", %{conn: conn} do
assert %{"error" => "Not found"} == json_response_and_validate_schema(conn, 404)
end
+
+ test "requires admin:read:accounts or broader scope",
+ %{admin: admin} do
+ user = insert(:user)
+ url = "/api/pleroma/admin/users/#{user.nickname}"
+
+ good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
+ good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+ good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
+
+ bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
+ bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
+ bad_token3 = nil
+
+ for good_token <- [good_token1, good_token2, good_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response_and_validate_schema(conn, 200)
+ end
+
+ for good_token <- [good_token1, good_token2, good_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, nil)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(conn, :forbidden)
+ end
+
+ for bad_token <- [bad_token1, bad_token2, bad_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, bad_token)
+ |> get(url)
+
+ assert json_response_and_validate_schema(conn, :forbidden)
+ end
+ end
end
describe "/api/pleroma/admin/users/follow" do
@@ -378,6 +405,18 @@ test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
end
describe "GET /api/pleroma/admin/users" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:users_read])
+ end
+
+ test "returns 403 if not privileged with :users_read", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn = get(conn, "/api/pleroma/admin/users?page=1")
+
+ assert json_response(conn, :forbidden)
+ end
+
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
user2 = insert(:user, is_approved: false, registration_reason: "I'm a chill dude")
@@ -810,49 +849,9 @@ test "it omits relay user", %{admin: admin, conn: conn} do
end
end
- test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
- user_one = insert(:user, is_active: false)
- user_two = insert(:user, is_active: false)
-
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> patch(
- "/api/pleroma/admin/users/activate",
- %{nicknames: [user_one.nickname, user_two.nickname]}
- )
-
- response = json_response_and_validate_schema(conn, 200)
- assert Enum.map(response["users"], & &1["is_active"]) == [true, true]
-
- log_entry = Repo.one(ModerationLog)
-
- assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
- end
-
- test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
- user_one = insert(:user, is_active: true)
- user_two = insert(:user, is_active: true)
-
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> patch(
- "/api/pleroma/admin/users/deactivate",
- %{nicknames: [user_one.nickname, user_two.nickname]}
- )
-
- response = json_response_and_validate_schema(conn, 200)
- assert Enum.map(response["users"], & &1["is_active"]) == [false, false]
-
- log_entry = Repo.one(ModerationLog)
-
- assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
- end
-
test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_invites])
+
user_one = insert(:user, is_approved: false)
user_two = insert(:user, is_approved: false)
@@ -873,6 +872,21 @@ test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
"@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
end
+ test "PATCH /api/pleroma/admin/users/approve returns 403 if not privileged with :users_manage_invites",
+ %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/approve",
+ %{nicknames: ["user_one.nickname", "user_two.nickname"]}
+ )
+
+ assert json_response(conn, :forbidden)
+ end
+
test "PATCH /api/pleroma/admin/users/suggest", %{admin: admin, conn: conn} do
user1 = insert(:user, is_suggested: false)
user2 = insert(:user, is_suggested: false)
@@ -923,24 +937,113 @@ test "PATCH /api/pleroma/admin/users/unsuggest", %{admin: admin, conn: conn} do
"@#{admin.nickname} removed suggested users: @#{user1.nickname}, @#{user2.nickname}"
end
- test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
- user = insert(:user)
+ describe "user activation" do
+ test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
+ user_one = insert(:user, is_active: false)
+ user_two = insert(:user, is_active: false)
- assert json_response_and_validate_schema(conn, 200) ==
- user_response(
- user,
- %{"is_active" => !user.is_active}
- )
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/activate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
- log_entry = Repo.one(ModerationLog)
+ response = json_response_and_validate_schema(conn, 200)
+ assert Enum.map(response["users"], & &1["is_active"]) == [true, true]
- assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deactivated users: @#{user.nickname}"
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
+
+ user_one = insert(:user, is_active: true)
+ user_two = insert(:user, is_active: true)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/deactivate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
+
+ response = json_response_and_validate_schema(conn, 200)
+ assert Enum.map(response["users"], & &1["is_active"]) == [false, false]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
+ clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
+
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
+
+ assert json_response_and_validate_schema(conn, 200) ==
+ user_response(
+ user,
+ %{"is_active" => !user.is_active}
+ )
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deactivated users: @#{user.nickname}"
+ end
+
+ test "it requires privileged role :statuses_activation to activate", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/activate",
+ %{nicknames: ["user_one.nickname", "user_two.nickname"]}
+ )
+
+ assert json_response(conn, :forbidden)
+ end
+
+ test "it requires privileged role :statuses_activation to deactivate", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/deactivate",
+ %{nicknames: ["user_one.nickname", "user_two.nickname"]}
+ )
+
+ assert json_response(conn, :forbidden)
+ end
+
+ test "it requires privileged role :statuses_activation to toggle activation", %{conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/pleroma/admin/users/user.nickname/toggle_activation")
+
+ assert json_response(conn, :forbidden)
+ end
end
defp user_response(user, attrs \\ %{}) do
diff --git a/test/pleroma/web/api_spec/scopes/compiler_test.exs b/test/pleroma/web/api_spec/scopes/compiler_test.exs
new file mode 100644
index 000000000..99e1d343a
--- /dev/null
+++ b/test/pleroma/web/api_spec/scopes/compiler_test.exs
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Scopes.CompilerTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Web.ApiSpec.Scopes.Compiler
+
+ @dummy_response %{}
+
+ @data %{
+ paths: %{
+ "/mew" => %OpenApiSpex.PathItem{
+ post: %OpenApiSpex.Operation{
+ security: [%{"oAuth" => ["a:b:c"]}],
+ responses: @dummy_response
+ },
+ get: %OpenApiSpex.Operation{security: nil, responses: @dummy_response}
+ },
+ "/mew2" => %OpenApiSpex.PathItem{
+ post: %OpenApiSpex.Operation{
+ security: [%{"oAuth" => ["d:e", "f:g"]}],
+ responses: @dummy_response
+ },
+ get: %OpenApiSpex.Operation{security: nil, responses: @dummy_response}
+ }
+ }
+ }
+
+ describe "process_scope/1" do
+ test "gives all higher-level scopes" do
+ scopes = Compiler.process_scope("admin:read:accounts")
+
+ assert [_, _, _] = scopes
+ assert "admin" in scopes
+ assert "admin:read" in scopes
+ assert "admin:read:accounts" in scopes
+ end
+ end
+
+ describe "extract_all_scopes_from/1" do
+ test "extracts scopes" do
+ scopes = Compiler.extract_all_scopes_from(@data)
+
+ assert [_, _, _, _, _, _, _] = scopes
+ assert "a" in scopes
+ assert "a:b" in scopes
+ assert "a:b:c" in scopes
+ assert "d" in scopes
+ assert "d:e" in scopes
+ assert "f" in scopes
+ assert "f:g" in scopes
+ end
+ end
+end
diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs
new file mode 100644
index 000000000..02bc6cf3b
--- /dev/null
+++ b/test/pleroma/web/common_api/activity_draft_test.exs
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.ActivityDraft
+
+ import Pleroma.Factory
+
+ test "create/2 with a quote post" do
+ user = insert(:user)
+ another_user = insert(:user)
+
+ {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
+ {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
+ {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
+ {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+ {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id})
+ {:error, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: public.id})
+ end
+end
diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs
index b538c5979..27b1da1e3 100644
--- a/test/pleroma/web/common_api/utils_test.exs
+++ b/test/pleroma/web/common_api/utils_test.exs
@@ -178,6 +178,10 @@ test "links" do
code = "https://github.com/pragdave/earmark/"
{result, [], []} = Utils.format_input(code, "text/markdown")
assert result == ~s[#{code}
]
+
+ code = "https://github.com/~foo/bar"
+ {result, [], []} = Utils.format_input(code, "text/markdown")
+ assert result == ~s[#{code}
]
end
test "link with local mention" do
@@ -196,7 +200,7 @@ test "local mentions" do
{result, _, []} = Utils.format_input(code, "text/markdown")
assert result ==
- ~s[@mario @luigi yo what’s up?
]
+ ~s[@mario @luigi yo what's up?
]
end
test "remote mentions" do
@@ -207,7 +211,7 @@ test "remote mentions" do
{result, _, []} = Utils.format_input(code, "text/markdown")
assert result ==
- ~s[@mario @luigi yo what’s up?
]
+ ~s[@mario @luigi yo what's up?
]
end
test "raw HTML" do
@@ -225,7 +229,7 @@ test "rulers" do
test "blockquote" do
code = ~s[> whoms't are you quoting?]
{result, [], []} = Utils.format_input(code, "text/markdown")
- assert result == "whoms’t are you quoting?
"
+ assert result == "whoms't are you quoting?
"
end
test "code" do
@@ -582,41 +586,61 @@ test "returns recipients when object not found" do
end
end
- describe "attachments_from_ids_descs/2" do
+ describe "attachments_from_ids_descs/3" do
test "returns [] when attachment ids is empty" do
- assert Utils.attachments_from_ids_descs([], "{}") == []
+ assert Utils.attachments_from_ids_descs([], "{}", nil) == []
end
test "returns list attachments with desc" do
- object = insert(:note)
+ user = insert(:user)
+ object = insert(:attachment, %{user: user})
desc = Jason.encode!(%{object.id => "test-desc"})
- assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [
+ assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc, user) == [
Map.merge(object.data, %{"name" => "test-desc"})
]
end
end
- describe "attachments_from_ids/1" do
+ describe "attachments_from_ids/2" do
test "returns attachments with descs" do
- object = insert(:note)
+ user = insert(:user)
+ object = insert(:attachment, %{user: user})
desc = Jason.encode!(%{object.id => "test-desc"})
- assert Utils.attachments_from_ids(%{
- media_ids: ["#{object.id}"],
- descriptions: desc
- }) == [
+ assert Utils.attachments_from_ids(
+ %{
+ media_ids: ["#{object.id}"],
+ descriptions: desc
+ },
+ user
+ ) == [
Map.merge(object.data, %{"name" => "test-desc"})
]
end
test "returns attachments without descs" do
- object = insert(:note)
- assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data]
+ user = insert(:user)
+ object = insert(:attachment, %{user: user})
+ assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user) == [object.data]
end
test "returns [] when not pass media_ids" do
- assert Utils.attachments_from_ids(%{}) == []
+ assert Utils.attachments_from_ids(%{}, nil) == []
+ end
+
+ test "returns [] when media_ids not belong to current user" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ object = insert(:attachment, %{user: user})
+
+ assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user2) == []
+ end
+
+ test "checks that the object is of upload type" do
+ object = insert(:note)
+ assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, nil) == []
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 9d52bd93e..b21dd4e23 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.CommonAPITest do
use Oban.Testing, repo: Pleroma.Repo
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Chat
@@ -279,6 +279,24 @@ test "it reject messages via MRF" do
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
end
+
+ test "it reject messages with attachments not belonging to user" do
+ author = insert(:user)
+ not_author = insert(:user)
+ recipient = author
+
+ attachment = insert(:attachment, %{user: not_author})
+
+ {:error, message} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ "123",
+ media_id: attachment.id
+ )
+
+ assert message == :forbidden
+ end
end
describe "unblocking" do
@@ -331,7 +349,7 @@ test "it allows users to delete their posts" do
refute Activity.get_by_id(post.id)
end
- test "it does not allow a user to delete their posts" do
+ test "it does not allow a user to delete posts from another user" do
user = insert(:user)
other_user = insert(:user)
@@ -341,7 +359,8 @@ test "it does not allow a user to delete their posts" do
assert Activity.get_by_id(post.id)
end
- test "it allows moderators to delete other user's posts" do
+ test "it allows privileged users to delete other user's posts" do
+ clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
@@ -353,19 +372,20 @@ test "it allows moderators to delete other user's posts" do
refute Activity.get_by_id(post.id)
end
- test "it allows admins to delete other user's posts" do
+ test "it doesn't allow unprivileged mods or admins to delete other user's posts" do
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [])
user = insert(:user)
- moderator = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true, is_admin: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
- assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
- assert delete.local
-
- refute Activity.get_by_id(post.id)
+ assert {:error, "Could not delete"} = CommonAPI.delete(post.id, moderator)
+ assert Activity.get_by_id(post.id)
end
- test "superusers deleting non-local posts won't federate the delete" do
+ test "privileged users deleting non-local posts won't federate the delete" do
+ clear_config([:instance, :admin_privileges], [:messages_delete])
# This is the user of the ingested activity
_user =
insert(:user,
@@ -374,7 +394,7 @@ test "superusers deleting non-local posts won't federate the delete" do
last_refreshed_at: NaiveDateTime.utc_now()
)
- moderator = insert(:user, is_admin: true)
+ admin = insert(:user, is_admin: true)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@@ -384,13 +404,27 @@ test "superusers deleting non-local posts won't federate the delete" do
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
- assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
+ assert {:ok, delete} = CommonAPI.delete(post.id, admin)
assert delete.local
refute called(Pleroma.Web.Federator.publish(:_))
end
refute Activity.get_by_id(post.id)
end
+
+ test "it allows privileged users to delete banned user's posts" do
+ clear_config([:instance, :moderator_privileges], [:messages_delete])
+ user = insert(:user)
+ moderator = insert(:user, is_moderator: true)
+
+ {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
+ User.set_activation(user, false)
+
+ assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
+ assert delete.local
+
+ refute Activity.get_by_id(post.id)
+ end
end
test "favoriting race condition" do
@@ -516,6 +550,36 @@ test "it de-duplicates tags" do
assert Object.tags(object) == ["2hu"]
end
+ test "zwnj is treated as word character" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{status: "#ساٴينس"})
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert Object.tags(object) == ["ساٴينس"]
+ end
+
+ test "allows lang attribute" do
+ user = insert(:user)
+ text = ~s{something random
}
+
+ {:ok, activity} = CommonAPI.post(user, %{status: text, content_type: "text/html"})
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert object.data["content"] == text
+ end
+
+ test "double dot in link is allowed" do
+ user = insert(:user)
+ text = "https://example.to/something..mp3"
+ {:ok, activity} = CommonAPI.post(user, %{status: text})
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert object.data["content"] == "#{text} "
+ end
+
test "it adds emoji in the object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
@@ -732,6 +796,65 @@ test "it can handle activities that expire" do
scheduled_at: expires_at
)
end
+
+ test "it allows quote posting" do
+ user = insert(:user)
+
+ {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id})
+
+ quoted = Object.normalize(quoted)
+ quote_post = Object.normalize(quote_post)
+
+ assert quote_post.data["quoteUrl"] == quoted.data["id"]
+
+ # The OP is not mentioned
+ refute quoted.data["actor"] in quote_post.data["to"]
+ end
+
+ test "quote posting with explicit addressing doesn't mention the OP" do
+ user = insert(:user)
+
+ {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
+
+ {:ok, quote_post} =
+ CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []})
+
+ assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
+ end
+
+ test "quote posting visibility" do
+ user = insert(:user)
+ another_user = insert(:user)
+
+ {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
+ {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
+ {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
+ {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+ {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id})
+ {:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id})
+ end
+
+ test "it properly mentions punycode domain" do
+ user = insert(:user)
+
+ _mentioned_user =
+ insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"})
+
+ assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"]
+ end
end
describe "reactions" do
@@ -1307,7 +1430,7 @@ test "cancels a pending follow for a local user" do
test "cancels a pending follow for a remote user" do
follower = insert(:user)
- followed = insert(:user, is_locked: true, local: false, ap_enabled: true)
+ followed = insert(:user, is_locked: true, local: false)
assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed)
diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs
index 41d1c5d5e..6826e6c2f 100644
--- a/test/pleroma/web/federator_test.exs
+++ b/test/pleroma/web/federator_test.exs
@@ -78,16 +78,14 @@ test "it federates only to reachable instances via AP" do
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
- inbox: inbox1,
- ap_enabled: true
+ inbox: inbox1
})
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
- inbox: inbox2,
- ap_enabled: true
+ inbox: inbox2
})
dt = NaiveDateTime.utc_now()
@@ -133,7 +131,7 @@ test "successfully processes incoming AP docs with correct origin" do
assert {:ok, _activity} = ObanHelpers.perform(job)
assert {:ok, job} = Federator.incoming_ap_doc(params)
- assert {:error, :already_present} = ObanHelpers.perform(job)
+ assert {:cancel, :already_present} = ObanHelpers.perform(job)
end
test "rejects incoming AP docs with incorrect origin" do
diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs
index de32d3d4b..d3c4108de 100644
--- a/test/pleroma/web/feed/user_controller_test.exs
+++ b/test/pleroma/web/feed/user_controller_test.exs
@@ -57,9 +57,23 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
)
note_activity2 = insert(:note_activity, note: note2)
+
+ note3 =
+ insert(:note,
+ user: user,
+ data: %{
+ "content" => "This note tests whether HTML entities are truncated properly",
+ "summary" => "Won't, didn't fail",
+ "inReplyTo" => note_activity2.id
+ }
+ )
+
+ _note_activity3 = insert(:note_activity, note: note3)
object = Object.normalize(note_activity, fetch: false)
- [user: user, object: object, max_id: note_activity2.id]
+ encoded_title = FeedView.activity_title(note3.data)
+
+ [user: user, object: object, max_id: note_activity2.id, encoded_title: encoded_title]
end
test "gets an atom feed", %{conn: conn, user: user, object: object, max_id: max_id} do
@@ -74,7 +88,7 @@ test "gets an atom feed", %{conn: conn, user: user, object: object, max_id: max_
|> SweetXml.parse()
|> SweetXml.xpath(~x"//entry/title/text()"l)
- assert activity_titles == ['2hu', '2hu & as']
+ assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as']
assert resp =~ FeedView.escape(object.data["content"])
assert resp =~ FeedView.escape(object.data["summary"])
assert resp =~ FeedView.escape(object.data["context"])
@@ -105,7 +119,7 @@ test "gets a rss feed", %{conn: conn, user: user, object: object, max_id: max_id
|> SweetXml.parse()
|> SweetXml.xpath(~x"//item/title/text()"l)
- assert activity_titles == ['2hu', '2hu & as']
+ assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as']
assert resp =~ FeedView.escape(object.data["content"])
assert resp =~ FeedView.escape(object.data["summary"])
assert resp =~ FeedView.escape(object.data["context"])
@@ -176,6 +190,30 @@ test "does not require authentication on non-federating instances", %{conn: conn
|> get("/users/#{user.nickname}/feed.rss")
|> response(200)
end
+
+ test "does not mangle HTML entities midway", %{
+ conn: conn,
+ user: user,
+ object: object,
+ encoded_title: encoded_title
+ } do
+ resp =
+ conn
+ |> put_req_header("accept", "application/atom+xml")
+ |> get(user_feed_path(conn, :feed, user.nickname))
+ |> response(200)
+
+ activity_titles =
+ resp
+ |> SweetXml.parse()
+ |> SweetXml.xpath(~x"//entry/title/text()"l)
+
+ assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as']
+ assert resp =~ FeedView.escape(object.data["content"])
+ assert resp =~ FeedView.escape(object.data["summary"])
+ assert resp =~ FeedView.escape(object.data["context"])
+ assert resp =~ encoded_title
+ end
end
# Note: see ActivityPubControllerTest for JSON format tests
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index 958b7f76f..128e60b0a 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -2031,6 +2031,39 @@ test "getting a list of blocks" do
assert [%{"id" => ^id1}] = result
end
+ test "list of blocks with with_relationships parameter" do
+ %{user: user, conn: conn} = oauth_access(["read:blocks"])
+ %{id: id1} = other_user1 = insert(:user)
+ %{id: id2} = other_user2 = insert(:user)
+ %{id: id3} = other_user3 = insert(:user)
+
+ {:ok, _, _} = User.follow(other_user1, user)
+ {:ok, _, _} = User.follow(other_user2, user)
+ {:ok, _, _} = User.follow(other_user3, user)
+
+ {:ok, _} = User.block(user, other_user1)
+ {:ok, _} = User.block(user, other_user2)
+ {:ok, _} = User.block(user, other_user3)
+
+ assert [
+ %{
+ "id" => ^id3,
+ "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
+ },
+ %{
+ "id" => ^id2,
+ "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
+ },
+ %{
+ "id" => ^id1,
+ "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
+ }
+ ] =
+ conn
+ |> get("/api/v1/blocks?with_relationships=true")
+ |> json_response_and_validate_schema(200)
+ end
+
test "account lookup", %{conn: conn} do
%{nickname: acct} = insert(:user, %{nickname: "nickname"})
%{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"})
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
index 13e3ffc0a..a556ef6a8 100644
--- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -92,4 +92,18 @@ test "get peers", %{conn: conn} do
assert ["peer1.com", "peer2.com"] == Enum.sort(result)
end
+
+ test "instance languages", %{conn: conn} do
+ assert %{"languages" => ["en"]} =
+ conn
+ |> get("/api/v1/instance")
+ |> json_response_and_validate_schema(200)
+
+ clear_config([:instance, :languages], ["aa", "bb"])
+
+ assert %{"languages" => ["aa", "bb"]} =
+ conn
+ |> get("/api/v1/instance")
+ |> json_response_and_validate_schema(200)
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
index 79d52bb2f..750296230 100644
--- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
@@ -122,6 +122,23 @@ test "/api/v2/media, upload_limit", %{conn: conn, user: user} do
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
+
+ test "Do not allow nested filename", %{conn: conn, image: image} do
+ image = %Plug.Upload{
+ image
+ | filename: "../../../../../nested/file.jpg"
+ }
+
+ desc = "Description of the image"
+
+ media =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/v1/media", %{"file" => image, "description" => desc})
+ |> json_response_and_validate_schema(:ok)
+
+ refute Regex.match?(~r"/nested/", media["url"])
+ end
end
describe "Update media description" do
diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
index 403628488..1524df98f 100644
--- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
alias Pleroma.Notification
alias Pleroma.Repo
@@ -74,12 +74,15 @@ test "by default, does not contain pleroma:chat_mention" do
end
test "by default, does not contain pleroma:report" do
- %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
+
+ user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
- user
- |> User.admin_api_update(%{is_moderator: true})
+ {:ok, user} = user |> User.admin_api_update(%{is_moderator: true})
+
+ %{conn: conn} = oauth_access(["read:notifications"], user: user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
@@ -101,6 +104,39 @@ test "by default, does not contain pleroma:report" do
assert [_] = result
end
+ test "Pleroma:report is hidden for non-privileged users" do
+ clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
+
+ user = insert(:user)
+ other_user = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, user} = user |> User.admin_api_update(%{is_moderator: true})
+
+ %{conn: conn} = oauth_access(["read:notifications"], user: user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
+
+ {:ok, _report} =
+ CommonAPI.report(third_user, %{account_id: other_user.id, status_ids: [activity.id]})
+
+ result =
+ conn
+ |> get("/api/v1/notifications?include_types[]=pleroma:report")
+ |> json_response_and_validate_schema(200)
+
+ assert [_] = result
+
+ clear_config([:instance, :moderator_privileges], [])
+
+ result =
+ conn
+ |> get("/api/v1/notifications?include_types[]=pleroma:report")
+ |> json_response_and_validate_schema(200)
+
+ assert [] == result
+ end
+
test "excludes mentions from blockers when blockers_visible is false" do
clear_config([:activitypub, :blockers_visible], false)
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index dbb840574..de3b52e26 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
@@ -125,6 +125,28 @@ test "posting a status", %{conn: conn} do
)
end
+ test "posting a quote post", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"})
+ %{data: %{"id" => quote_url}} = Object.normalize(activity)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "indeed",
+ "quote_id" => activity_id
+ })
+
+ assert %{
+ "id" => id,
+ "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url}
+ } = json_response_and_validate_schema(conn, 200)
+
+ assert Activity.get_by_id(id)
+ end
+
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
@@ -626,7 +648,10 @@ test "option limit is enforced", %{conn: conn} do
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
- "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
+ "poll" => %{
+ "options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
+ "expires_in" => 1
+ }
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
@@ -642,7 +667,7 @@ test "option character limit is enforced", %{conn: conn} do
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
- "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
+ "options" => [String.duplicate(".", limit + 1), "lol"],
"expires_in" => 1
}
})
@@ -724,6 +749,32 @@ test "scheduled poll", %{conn: conn} do
assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3
end
+
+ test "cannot have only one option", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "desu~",
+ "poll" => %{"options" => ["mew"], "expires_in" => 1}
+ })
+
+ %{"error" => error} = json_response_and_validate_schema(conn, 422)
+ assert error == "Poll must contain at least 2 options"
+ end
+
+ test "cannot have only duplicated options", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "desu~",
+ "poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
+ })
+
+ %{"error" => error} = json_response_and_validate_schema(conn, 422)
+ assert error == "Poll must contain at least 2 options"
+ end
end
test "get a status" do
@@ -742,6 +793,49 @@ defp local_and_remote_activities do
{:ok, local: local, remote: remote}
end
+ defp local_and_remote_context_activities do
+ local_user_1 = insert(:user)
+ local_user_2 = insert(:user)
+ remote_user = insert(:user, local: false)
+
+ {:ok, %{id: id1, data: %{"context" => context}}} =
+ CommonAPI.post(local_user_1, %{status: "post"})
+
+ {:ok, %{id: id2} = post} =
+ CommonAPI.post(local_user_2, %{status: "local reply", in_reply_to_status_id: id1})
+
+ params = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => remote_user.ap_id,
+ "type" => "Create",
+ "context" => context,
+ "id" => "#{remote_user.ap_id}/activities/1",
+ "inReplyTo" => post.data["id"],
+ "object" => %{
+ "type" => "Note",
+ "content" => "remote reply",
+ "context" => context,
+ "id" => "#{remote_user.ap_id}/objects/1",
+ "attributedTo" => remote_user.ap_id,
+ "to" => [
+ local_user_1.ap_id,
+ local_user_2.ap_id,
+ "https://www.w3.org/ns/activitystreams#Public"
+ ]
+ },
+ "to" => [
+ local_user_1.ap_id,
+ local_user_2.ap_id,
+ "https://www.w3.org/ns/activitystreams#Public"
+ ]
+ }
+
+ {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params)
+ {:ok, remote_activity} = ObanHelpers.perform(job)
+
+ %{locals: [id1, id2], remote: remote_activity.id, context: context}
+ end
+
describe "status with restrict unauthenticated activities for local and remote" do
setup do: local_and_remote_activities()
@@ -928,6 +1022,230 @@ test "if user is authenticated", %{local: local, remote: remote} do
end
end
+ describe "getting status contexts restricted unauthenticated for local and remote" do
+ setup do: local_and_remote_context_activities()
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
+
+ test "if user is unauthenticated", %{conn: conn, locals: [post_id, _]} do
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ assert json_response_and_validate_schema(res_conn, 200) == %{
+ "ancestors" => [],
+ "descendants" => []
+ }
+ end
+
+ test "if user is unauthenticated reply", %{conn: conn, locals: [_, reply_id]} do
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ assert json_response_and_validate_schema(res_conn, 200) == %{
+ "ancestors" => [],
+ "descendants" => []
+ }
+ end
+
+ test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ %{"ancestors" => [], "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert reply_id in descendant_ids
+ assert remote_reply_id in descendant_ids
+ end
+
+ test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ %{"ancestors" => ancestors, "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ ancestor_ids =
+ ancestors
+ |> Enum.map(& &1["id"])
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert post_id in ancestor_ids
+ assert remote_reply_id in descendant_ids
+ end
+ end
+
+ describe "getting status contexts restricted unauthenticated for local" do
+ setup do: local_and_remote_context_activities()
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :remote], false)
+
+ test "if user is unauthenticated", %{
+ conn: conn,
+ locals: [post_id, reply_id],
+ remote: remote_reply_id
+ } do
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ %{"ancestors" => [], "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert reply_id not in descendant_ids
+ assert remote_reply_id in descendant_ids
+ end
+
+ test "if user is unauthenticated reply", %{
+ conn: conn,
+ locals: [post_id, reply_id],
+ remote: remote_reply_id
+ } do
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ %{"ancestors" => ancestors, "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ ancestor_ids =
+ ancestors
+ |> Enum.map(& &1["id"])
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert post_id not in ancestor_ids
+ assert remote_reply_id in descendant_ids
+ end
+
+ test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ %{"ancestors" => [], "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert reply_id in descendant_ids
+ assert remote_reply_id in descendant_ids
+ end
+
+ test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ %{"ancestors" => ancestors, "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ ancestor_ids =
+ ancestors
+ |> Enum.map(& &1["id"])
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert post_id in ancestor_ids
+ assert remote_reply_id in descendant_ids
+ end
+ end
+
+ describe "getting status contexts restricted unauthenticated for remote" do
+ setup do: local_and_remote_context_activities()
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :local], false)
+
+ setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
+
+ test "if user is unauthenticated", %{
+ conn: conn,
+ locals: [post_id, reply_id],
+ remote: remote_reply_id
+ } do
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ %{"ancestors" => [], "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert reply_id in descendant_ids
+ assert remote_reply_id not in descendant_ids
+ end
+
+ test "if user is unauthenticated reply", %{
+ conn: conn,
+ locals: [post_id, reply_id],
+ remote: remote_reply_id
+ } do
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ %{"ancestors" => ancestors, "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ ancestor_ids =
+ ancestors
+ |> Enum.map(& &1["id"])
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert post_id in ancestor_ids
+ assert remote_reply_id not in descendant_ids
+ end
+
+ test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
+
+ %{"ancestors" => [], "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ reply_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert reply_id in reply_ids
+ assert remote_reply_id in reply_ids
+ end
+
+ test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
+ %{conn: conn} = oauth_access(["read"])
+ res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
+
+ %{"ancestors" => ancestors, "descendants" => descendants} =
+ json_response_and_validate_schema(res_conn, 200)
+
+ ancestor_ids =
+ ancestors
+ |> Enum.map(& &1["id"])
+
+ descendant_ids =
+ descendants
+ |> Enum.map(& &1["id"])
+
+ assert post_id in ancestor_ids
+ assert remote_reply_id in descendant_ids
+ end
+ end
+
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
@@ -971,9 +1289,10 @@ test "when you didn't create it" do
assert Activity.get_by_id(activity.id) == activity
end
- test "when you're an admin", %{conn: conn} do
+ test "when you're privileged to", %{conn: conn} do
+ clear_config([:instance, :moderator_privileges], [:messages_delete])
activity = insert(:note_activity)
- user = insert(:user, is_admin: true)
+ user = insert(:user, is_moderator: true)
res_conn =
conn
@@ -989,8 +1308,11 @@ test "when you're an admin", %{conn: conn} do
refute Activity.get_by_id(activity.id)
end
- test "when you're a moderator", %{conn: conn} do
- activity = insert(:note_activity)
+ test "when you're privileged and the user is banned", %{conn: conn} do
+ clear_config([:instance, :moderator_privileges], [:messages_delete])
+ posting_user = insert(:user, is_active: false)
+ refute posting_user.is_active
+ activity = insert(:note_activity, user: posting_user)
user = insert(:user, is_moderator: true)
res_conn =
diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs
index d5fac7e25..45412bb34 100644
--- a/test/pleroma/web/mastodon_api/update_credentials_test.exs
+++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs
@@ -97,6 +97,42 @@ test "updates the user's bio", %{conn: conn} do
assert user.raw_bio == raw_bio
end
+ test "updating bio honours bio limit", %{conn: conn} do
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+
+ raw_bio = String.duplicate(".", bio_limit + 1)
+
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio})
+
+ assert %{"error" => "Bio is too long"} = json_response_and_validate_schema(conn, 413)
+ end
+
+ test "updating name honours name limit", %{conn: conn} do
+ name_limit = Config.get([:instance, :user_name_length], 100)
+
+ name = String.duplicate(".", name_limit + 1)
+
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => name})
+
+ assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413)
+ end
+
+ test "when both name and bio exceeds the limit, display name error", %{conn: conn} do
+ name_limit = Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+
+ name = String.duplicate(".", name_limit + 1)
+ raw_bio = String.duplicate(".", bio_limit + 1)
+
+ conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "display_name" => name,
+ "note" => raw_bio
+ })
+
+ assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413)
+ end
+
test "updates the user's locking status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})
@@ -375,7 +411,9 @@ test "updates the user's background, upload_limit, returns a HTTP 413", %{
"pleroma_background_image" => new_background_oversized
})
- assert user_response = json_response_and_validate_schema(res, 413)
+ assert %{"error" => "File is too large"} == json_response_and_validate_schema(res, 413)
+
+ user = Repo.get(User, user.id)
assert user.background == %{}
clear_config([:instance, :upload_limit], upload_limit)
@@ -383,6 +421,34 @@ test "updates the user's background, upload_limit, returns a HTTP 413", %{
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
+ test "Strip / from upload files", %{user: user, conn: conn} do
+ new_image = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "../../../../nested/an_image.jpg"
+ }
+
+ assert user.avatar == %{}
+
+ res =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "avatar" => new_image,
+ "header" => new_image,
+ "pleroma_background_image" => new_image
+ })
+
+ assert user_response = json_response_and_validate_schema(res, 200)
+ assert user_response["avatar"]
+ assert user_response["header"]
+ assert user_response["pleroma"]["background_image"]
+ refute Regex.match?(~r"/nested/", user_response["avatar"])
+ refute Regex.match?(~r"/nested/", user_response["header"])
+ refute Regex.match?(~r"/nested/", user_response["pleroma"]["background_image"])
+
+ user = User.get_by_id(user.id)
+ refute user.avatar == %{}
+ end
+
test "requires 'write:accounts' permission" do
token1 = insert(:oauth_token, scopes: ["read"])
token2 = insert(:oauth_token, scopes: ["write", "follow"])
@@ -565,17 +631,17 @@ test "update fields when invalid request", %{conn: conn} do
fields = [%{"name" => "foo", "value" => long_value}]
- assert %{"error" => "Invalid request"} ==
+ assert %{"error" => "One or more field entries are too long"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
- |> json_response_and_validate_schema(403)
+ |> json_response_and_validate_schema(413)
fields = [%{"name" => long_name, "value" => "bar"}]
- assert %{"error" => "Invalid request"} ==
+ assert %{"error" => "One or more field entries are too long"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
- |> json_response_and_validate_schema(403)
+ |> json_response_and_validate_schema(413)
clear_config([:instance, :max_account_fields], 1)
@@ -584,10 +650,10 @@ test "update fields when invalid request", %{conn: conn} do
%{"name" => "link", "value" => "cofe.io"}
]
- assert %{"error" => "Invalid request"} ==
+ assert %{"error" => "Too many field entries"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
- |> json_response_and_validate_schema(403)
+ |> json_response_and_validate_schema(413)
end
end
diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs
index 692ec8c92..3bb4970ca 100644
--- a/test/pleroma/web/mastodon_api/views/account_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
alias Pleroma.User
alias Pleroma.UserRelationship
@@ -84,6 +84,7 @@ test "Represent a user account" do
tags: [],
is_admin: false,
is_moderator: false,
+ privileges: [],
is_suggested: false,
hide_favorites: true,
hide_followers: false,
@@ -99,6 +100,147 @@ test "Represent a user account" do
assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
+ describe "roles and privileges" do
+ setup do
+ clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator])
+ clear_config([:instance, :admin_privileges], [:cofe, :only_admin])
+
+ %{
+ user: insert(:user),
+ moderator: insert(:user, is_moderator: true),
+ admin: insert(:user, is_admin: true),
+ moderator_admin: insert(:user, is_moderator: true, is_admin: true),
+ user_no_show_roles: insert(:user, show_role: false),
+ moderator_admin_no_show_roles:
+ insert(:user, is_moderator: true, is_admin: true, show_role: false)
+ }
+ end
+
+ test "shows roles and privileges when show_role: true", %{
+ user: user,
+ moderator: moderator,
+ admin: admin,
+ moderator_admin: moderator_admin,
+ user_no_show_roles: user_no_show_roles,
+ moderator_admin_no_show_roles: moderator_admin_no_show_roles
+ } do
+ assert %{pleroma: %{is_moderator: false, is_admin: false}} =
+ AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+
+ assert [] ==
+ AccountView.render("show.json", %{user: user, skip_visibility_check: true})[
+ :pleroma
+ ][:privileges]
+ |> Enum.sort()
+
+ assert %{pleroma: %{is_moderator: true, is_admin: false}} =
+ AccountView.render("show.json", %{user: moderator, skip_visibility_check: true})
+
+ assert [:cofe, :only_moderator] ==
+ AccountView.render("show.json", %{user: moderator, skip_visibility_check: true})[
+ :pleroma
+ ][:privileges]
+ |> Enum.sort()
+
+ assert %{pleroma: %{is_moderator: false, is_admin: true}} =
+ AccountView.render("show.json", %{user: admin, skip_visibility_check: true})
+
+ assert [:cofe, :only_admin] ==
+ AccountView.render("show.json", %{user: admin, skip_visibility_check: true})[
+ :pleroma
+ ][:privileges]
+ |> Enum.sort()
+
+ assert %{pleroma: %{is_moderator: true, is_admin: true}} =
+ AccountView.render("show.json", %{
+ user: moderator_admin,
+ skip_visibility_check: true
+ })
+
+ assert [:cofe, :only_admin, :only_moderator] ==
+ AccountView.render("show.json", %{
+ user: moderator_admin,
+ skip_visibility_check: true
+ })[:pleroma][:privileges]
+ |> Enum.sort()
+
+ refute match?(
+ %{pleroma: %{is_moderator: _}},
+ AccountView.render("show.json", %{
+ user: user_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+
+ refute match?(
+ %{pleroma: %{is_admin: _}},
+ AccountView.render("show.json", %{
+ user: user_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+
+ refute match?(
+ %{pleroma: %{privileges: _}},
+ AccountView.render("show.json", %{
+ user: user_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+
+ refute match?(
+ %{pleroma: %{is_moderator: _}},
+ AccountView.render("show.json", %{
+ user: moderator_admin_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+
+ refute match?(
+ %{pleroma: %{is_admin: _}},
+ AccountView.render("show.json", %{
+ user: moderator_admin_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+
+ refute match?(
+ %{pleroma: %{privileges: _}},
+ AccountView.render("show.json", %{
+ user: moderator_admin_no_show_roles,
+ skip_visibility_check: true
+ })
+ )
+ end
+
+ test "shows roles and privileges when viewing own account, even when show_role: false", %{
+ user_no_show_roles: user_no_show_roles,
+ moderator_admin_no_show_roles: moderator_admin_no_show_roles
+ } do
+ assert %{pleroma: %{is_moderator: false, is_admin: false, privileges: []}} =
+ AccountView.render("show.json", %{
+ user: user_no_show_roles,
+ skip_visibility_check: true,
+ for: user_no_show_roles
+ })
+
+ assert %{
+ pleroma: %{
+ is_moderator: true,
+ is_admin: true,
+ privileges: privileges
+ }
+ } =
+ AccountView.render("show.json", %{
+ user: moderator_admin_no_show_roles,
+ skip_visibility_check: true,
+ for: moderator_admin_no_show_roles
+ })
+
+ assert [:cofe, :only_admin, :only_moderator] == privileges |> Enum.sort()
+ end
+ end
+
describe "favicon" do
setup do
[user: insert(:user)]
@@ -186,6 +328,7 @@ test "Represent a Service(bot) account" do
tags: [],
is_admin: false,
is_moderator: false,
+ privileges: [],
is_suggested: false,
hide_favorites: true,
hide_followers: false,
@@ -214,8 +357,10 @@ test "Represent a Funkwhale channel" do
assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions"
end
- test "Represent a deactivated user for an admin" do
- admin = insert(:user, is_admin: true)
+ test "Represent a deactivated user for a privileged user" do
+ clear_config([:instance, :moderator_privileges], [:users_manage_activation_state])
+
+ admin = insert(:user, is_moderator: true)
deactivated_user = insert(:user, is_active: false)
represented = AccountView.render("show.json", %{user: deactivated_user, for: admin})
assert represented[:pleroma][:deactivated] == true
diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs
index d3d74f5cd..ddbe4557f 100644
--- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Chat
@@ -190,7 +190,47 @@ test "EmojiReact notification" do
emoji: "☕",
account: AccountView.render("show.json", %{user: other_user, for: user}),
status: StatusView.render("show.json", %{activity: activity, for: user}),
- created_at: Utils.to_masto_date(notification.inserted_at)
+ created_at: Utils.to_masto_date(notification.inserted_at),
+ emoji_url: nil
+ }
+
+ test_notifications_rendering([notification], user, [expected])
+ end
+
+ test "EmojiReact custom emoji notification" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ note =
+ insert(:note,
+ user: user,
+ data: %{
+ "reactions" => [
+ ["👍", [user.ap_id], nil],
+ ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino walking.gif"]
+ ]
+ }
+ )
+
+ activity = insert(:note_activity, note: note, user: user)
+
+ {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "dinosaur")
+
+ activity = Repo.get(Activity, activity.id)
+
+ [notification] = Notification.for_user(user)
+
+ assert notification
+
+ expected = %{
+ id: to_string(notification.id),
+ pleroma: %{is_seen: false, is_muted: false},
+ type: "pleroma:emoji_reaction",
+ emoji: ":dinosaur:",
+ account: AccountView.render("show.json", %{user: other_user, for: user}),
+ status: StatusView.render("show.json", %{activity: activity, for: user}),
+ created_at: Utils.to_masto_date(notification.inserted_at),
+ emoji_url: "http://localhost:4001/emoji/dino walking.gif"
}
test_notifications_rendering([notification], user, [expected])
@@ -218,9 +258,11 @@ test "Poll notification" do
end
test "Report notification" do
+ clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
+
reporting_user = insert(:user)
reported_user = insert(:user)
- {:ok, moderator_user} = insert(:user) |> User.admin_api_update(%{is_moderator: true})
+ moderator_user = insert(:user, is_moderator: true)
{:ok, activity} = CommonAPI.report(reporting_user, %{account_id: reported_user.id})
{:ok, [notification]} = Notification.create_notifications(activity)
diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs
index e5e510d33..07a65a3bc 100644
--- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs
@@ -48,7 +48,7 @@ test "A scheduled activity with a media attachment" do
id: to_string(scheduled_activity.id),
media_attachments:
%{media_ids: [upload.id]}
- |> Utils.attachments_from_ids()
+ |> Utils.attachments_from_ids(user)
|> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})),
params: %{
in_reply_to_id: to_string(activity.id),
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index f76b115b7..baa9b32f5 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -35,16 +35,26 @@ test "has an emoji reaction list" do
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
+
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: false},
- %{name: "🍵", count: 1, me: false}
+ %{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
+ %{
+ count: 2,
+ me: false,
+ name: "dinosaur",
+ url: "http://localhost:4001/emoji/dino walking.gif",
+ account_ids: [other_user.id, user.id]
+ },
+ %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
@@ -52,8 +62,36 @@ test "has an emoji reaction list" do
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: true},
- %{name: "🍵", count: 1, me: false}
+ %{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
+ %{
+ count: 2,
+ me: true,
+ name: "dinosaur",
+ url: "http://localhost:4001/emoji/dino walking.gif",
+ account_ids: [other_user.id, user.id]
+ },
+ %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
+ ]
+ end
+
+ test "works with legacy-formatted reactions" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ note =
+ insert(:note,
+ user: user,
+ data: %{
+ "reactions" => [["😿", [other_user.ap_id]]]
+ }
+ )
+
+ activity = insert(:note_activity, user: user, note: note)
+
+ status = StatusView.render("show.json", activity: activity, for: user)
+
+ assert status[:pleroma][:emoji_reactions] == [
+ %{name: "😿", count: 1, me: false, url: nil, account_ids: [other_user.id]}
]
end
@@ -66,11 +104,10 @@ test "works correctly with badly formatted emojis" do
|> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}})
activity = Activity.get_by_id(activity.id)
-
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: true}
+ %{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]}
]
end
@@ -90,7 +127,7 @@ test "doesn't show reactions from muted and blocked users" do
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: false}
+ %{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
@@ -102,19 +139,25 @@ test "doesn't show reactions from muted and blocked users" do
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: false}
+ %{
+ name: "☕",
+ count: 2,
+ me: false,
+ url: nil,
+ account_ids: [third_user.id, other_user.id]
+ }
]
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: false}
+ %{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: other_user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: true}
+ %{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]}
]
end
@@ -283,6 +326,10 @@ test "a note activity" do
conversation_id: convo_id,
context: object_data["context"],
in_reply_to_account_acct: nil,
+ quote: nil,
+ quote_id: nil,
+ quote_url: nil,
+ quote_visible: false,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
@@ -379,6 +426,88 @@ test "a reply" do
assert status.in_reply_to_id == to_string(note.id)
end
+ test "a quote post" do
+ post = insert(:note_activity)
+ user = insert(:user)
+
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
+ {:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id})
+
+ status = StatusView.render("show.json", %{activity: quoted_quote_post})
+
+ assert status.pleroma.quote.id == to_string(quote_post.id)
+ assert status.pleroma.quote_id == to_string(quote_post.id)
+ assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"]
+ assert status.pleroma.quote_visible
+
+ # Quotes don't go more than one level deep
+ refute status.pleroma.quote.pleroma.quote
+ assert status.pleroma.quote.pleroma.quote_id == to_string(post.id)
+ assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"]
+ assert status.pleroma.quote.pleroma.quote_visible
+
+ # In an index
+ [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity})
+
+ assert status.pleroma.quote.id == to_string(quote_post.id)
+ end
+
+ test "quoted private post" do
+ user = insert(:user)
+
+ # Insert a private post
+ private = insert(:followers_only_note_activity, user: user)
+ private_object = Object.normalize(private)
+
+ # Create a public post quoting the private post
+ quote_private =
+ insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]}))
+
+ status = StatusView.render("show.json", %{activity: quote_private})
+
+ # The quote isn't rendered
+ refute status.pleroma.quote
+ assert status.pleroma.quote_url == private_object.data["id"]
+ refute status.pleroma.quote_visible
+
+ # After following the user, the quote is rendered
+ follower = insert(:user)
+ CommonAPI.follow(follower, user)
+
+ status = StatusView.render("show.json", %{activity: quote_private, for: follower})
+ assert status.pleroma.quote.id == to_string(private.id)
+ assert status.pleroma.quote_visible
+ end
+
+ test "quoted direct message" do
+ # Insert a direct message
+ direct = insert(:direct_note_activity)
+ direct_object = Object.normalize(direct)
+
+ # Create a public post quoting the direct message
+ quote_direct =
+ insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]}))
+
+ status = StatusView.render("show.json", %{activity: quote_direct})
+
+ # The quote isn't rendered
+ refute status.pleroma.quote
+ assert status.pleroma.quote_url == direct_object.data["id"]
+ refute status.pleroma.quote_visible
+ end
+
+ test "repost of quote post" do
+ post = insert(:note_activity)
+ user = insert(:user)
+
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
+ {:ok, repost} = CommonAPI.repeat(quote_post.id, user)
+
+ [status] = StatusView.render("index.json", %{activities: [repost], as: :activity})
+
+ assert status.reblog.pleroma.quote.id == to_string(post.id)
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
index 5246bf0c4..9ce092fd8 100644
--- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
@@ -6,7 +6,9 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
use Pleroma.Web.ConnCase
import Mock
+ import Mox
+ alias Pleroma.ReverseProxy.ClientMock
alias Pleroma.Web.MediaProxy
alias Plug.Conn
@@ -74,6 +76,20 @@ test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url}
assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url)
end
end
+
+ test "it applies sandbox CSP to MediaProxy requests", %{conn: conn} do
+ media_url = "https://lain.com/image.png"
+ media_proxy_url = MediaProxy.encode_url(media_url)
+
+ ClientMock
+ |> expect(:request, fn :get, ^media_url, _, _, _ ->
+ {:ok, 200, [{"content-type", "image/png"}]}
+ end)
+
+ %Conn{resp_headers: headers} = get(conn, media_proxy_url)
+
+ assert {"content-security-policy", "sandbox;"} in headers
+ end
end
describe "Media Preview Proxy" do
diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs
index cce4f3607..793669037 100644
--- a/test/pleroma/web/metadata/providers/rel_me_test.exs
+++ b/test/pleroma/web/metadata/providers/rel_me_test.exs
@@ -11,11 +11,24 @@ test "it renders all links with rel='me' from user bio" do
bio =
~s(https://some-link.com https://another-link.com )
- user = insert(:user, %{bio: bio})
+ fields = [
+ %{
+ "name" => "profile",
+ "value" => ~S(http://profile.com )
+ },
+ %{
+ "name" => "like",
+ "value" => ~S(http://cofe.io )
+ },
+ %{"name" => "foo", "value" => "bar"}
+ ]
+
+ user = insert(:user, %{bio: bio, fields: fields})
assert RelMe.build_tags(%{user: user}) == [
{:link, [rel: "me", href: "http://some3.com"], []},
- {:link, [rel: "me", href: "https://another-link.com"], []}
+ {:link, [rel: "me", href: "https://another-link.com"], []},
+ {:link, [rel: "me", href: "http://profile.com"], []}
]
end
end
diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs
index be4cfbe7b..f8d01c5c8 100644
--- a/test/pleroma/web/metadata/providers/twitter_card_test.exs
+++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs
@@ -182,7 +182,8 @@ test "it renders supported types of attachments and skips unknown types" do
{:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
{:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
{:meta, [name: "twitter:card", content: "summary_large_image"], []},
- {:meta, [name: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []},
+ {:meta, [name: "twitter:image", content: "https://pleroma.gov/tenshi.png"], []},
+ {:meta, [name: "twitter:image:alt", content: ""], []},
{:meta, [name: "twitter:player:width", content: "1280"], []},
{:meta, [name: "twitter:player:height", content: "1024"], []},
{:meta, [name: "twitter:card", content: "player"], []},
diff --git a/test/pleroma/web/metadata/utils_test.exs b/test/pleroma/web/metadata/utils_test.exs
index 85ef6033a..3daf852fb 100644
--- a/test/pleroma/web/metadata/utils_test.exs
+++ b/test/pleroma/web/metadata/utils_test.exs
@@ -72,7 +72,7 @@ test "it does not return old content after editing" do
end
end
- describe "scrub_html_and_truncate/2" do
+ describe "scrub_html_and_truncate/3" do
test "it returns text without encode HTML" do
assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!"
end
diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs
index 247ad7501..f474220be 100644
--- a/test/pleroma/web/node_info_test.exs
+++ b/test/pleroma/web/node_info_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.NodeInfoTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -40,6 +40,19 @@ test "nodeinfo shows staff accounts", %{conn: conn} do
assert admin.ap_id in result["metadata"]["staffAccounts"]
end
+ test "nodeinfo shows roles and privileges", %{conn: conn} do
+ clear_config([:instance, :moderator_privileges], [:cofe])
+ clear_config([:instance, :admin_privileges], [:suya, :cofe])
+
+ conn =
+ conn
+ |> get("/nodeinfo/2.1.json")
+
+ assert result = json_response(conn, 200)
+
+ assert %{"admin" => ["suya", "cofe"], "moderator" => ["cofe"]} == result["metadata"]["roles"]
+ end
+
test "nodeinfo shows restricted nicknames", %{conn: conn} do
conn =
conn
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
index 200ce3b68..540b452c7 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do
- use Pleroma.Web.ConnCase
+ use Pleroma.Web.ConnCase, async: false
import Mock
import Tesla.Mock
@@ -30,6 +30,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do
describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do
setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
pack_file = "#{@emoji_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
@@ -377,5 +378,32 @@ test "update with empty shortcode", %{admin_conn: admin_conn} do
})
|> json_response_and_validate_schema(:bad_request)
end
+
+ test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ file: %Plug.Upload{
+ filename: "shortcode.png",
+ path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png"
+ }
+ })
+ |> json_response(:forbidden)
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ shortcode: "blank",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(:forbidden)
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3")
+ |> json_response(:forbidden)
+ end
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
index d1fd1cbb0..1d5240639 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
@@ -99,6 +99,10 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do
end
describe "GET /api/pleroma/emoji/packs/remote" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
+ end
+
test "shareable instance", %{admin_conn: admin_conn, conn: conn} do
resp =
conn
@@ -136,6 +140,14 @@ test "non shareable instance", %{admin_conn: admin_conn} do
"error" => "The requested instance does not support sharing emoji packs"
}
end
+
+ test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/remote?url=https://example.com")
+ |> json_response(:forbidden)
+ end
end
describe "GET /api/pleroma/emoji/packs/archive?name=:name" do
@@ -170,6 +182,10 @@ test "non downloadable pack", %{conn: conn} do
end
describe "POST /api/pleroma/emoji/packs/download" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
+ end
+
test "shared pack from remote and non shared from fallback-src", %{
admin_conn: admin_conn,
conn: conn
@@ -344,10 +360,24 @@ test "other error", %{admin_conn: admin_conn} do
"The pack was not set as shared and there is no fallback src to download from"
}
end
+
+ test "it requires privileged role :emoji_manage_emoji", %{admin_conn: conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download", %{
+ url: "https://example.com",
+ name: "test_pack",
+ as: "test_pack2"
+ })
+ |> json_response(:forbidden)
+ end
end
describe "PATCH/update /api/pleroma/emoji/pack?name=:name" do
setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
pack_file = "#{@emoji_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
@@ -435,9 +465,25 @@ test "when the fallback source doesn't have all the files", ctx do
"error" => "The fallback archive does not have all files specified in pack.json"
}
end
+
+ test "it requires privileged role :emoji_manage_emoji", %{
+ admin_conn: conn,
+ new_data: new_data
+ } do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data})
+ |> json_response(:forbidden)
+ end
end
describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
+ end
+
test "returns an error on creates pack when file system not writable", %{
admin_conn: admin_conn
} do
@@ -520,6 +566,18 @@ test "with empty name", %{admin_conn: admin_conn} do
"error" => "pack name cannot be empty"
}
end
+
+ test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do
+ clear_config([:instance, :admin_privileges], [])
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/pack?name= ")
+ |> json_response(:forbidden)
+
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/pack?name= ")
+ |> json_response(:forbidden)
+ end
end
test "deleting nonexisting pack", %{admin_conn: admin_conn} do
@@ -578,6 +636,12 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
"blank2" => "blank.png",
"foo" => "blank.png"
}
+
+ clear_config([:instance, :admin_privileges], [])
+
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/import")
+ |> json_response(:forbidden)
end
describe "GET /api/pleroma/emoji/pack?name=:name" do
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
index 77c75b560..21e7d4839 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
@@ -17,23 +17,113 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+ note = insert(:note, user: user, data: %{"reactions" => [["👍", [other_user.ap_id], nil]]})
+ activity = insert(:note_activity, note: note, user: user)
result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
- |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
+ |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/\u26A0")
|> json_response_and_validate_schema(200)
- # We return the status, but this our implementation detail.
assert %{"id" => id} = result
assert to_string(activity.id) == id
assert result["pleroma"]["emoji_reactions"] == [
- %{"name" => "☕", "count" => 1, "me" => true}
+ %{
+ "name" => "👍",
+ "count" => 1,
+ "me" => true,
+ "url" => nil,
+ "account_ids" => [other_user.id]
+ },
+ %{
+ "name" => "\u26A0\uFE0F",
+ "count" => 1,
+ "me" => true,
+ "url" => nil,
+ "account_ids" => [other_user.id]
+ }
]
+ {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+
+ ObanHelpers.perform_all()
+
+ # Reacting with a custom emoji
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:")
+ |> json_response_and_validate_schema(200)
+
+ assert %{"id" => id} = result
+ assert to_string(activity.id) == id
+
+ assert result["pleroma"]["emoji_reactions"] == [
+ %{
+ "name" => "dinosaur",
+ "count" => 1,
+ "me" => true,
+ "url" => "http://localhost:4001/emoji/dino walking.gif",
+ "account_ids" => [other_user.id]
+ }
+ ]
+
+ # Reacting with a remote emoji
+ note =
+ insert(:note,
+ user: user,
+ data: %{
+ "reactions" => [
+ ["👍", [other_user.ap_id], nil],
+ ["wow", [other_user.ap_id], "https://remote/emoji/wow"]
+ ]
+ }
+ )
+
+ activity = insert(:note_activity, note: note, user: user)
+
+ result =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
+ |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
+ |> json_response(200)
+
+ assert result["pleroma"]["emoji_reactions"] == [
+ %{
+ "account_ids" => [other_user.id],
+ "count" => 1,
+ "me" => false,
+ "name" => "👍",
+ "url" => nil
+ },
+ %{
+ "name" => "wow@remote",
+ "count" => 2,
+ "me" => true,
+ "url" => "https://remote/emoji/wow",
+ "account_ids" => [user.id, other_user.id]
+ }
+ ]
+
+ # Reacting with a remote custom emoji that hasn't been reacted with yet
+ note =
+ insert(:note,
+ user: user
+ )
+
+ activity = insert(:note_activity, note: note, user: user)
+
+ assert conn
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
+ |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
+ |> json_response(400)
+
# Reacting with a non-emoji
assert conn
|> assign(:user, other_user)
@@ -46,8 +136,21 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+ note =
+ insert(:note,
+ user: user,
+ data: %{"reactions" => [["wow", [user.ap_id], "https://remote/emoji/wow"]]}
+ )
+
+ activity = insert(:note_activity, note: note, user: user)
+
+ ObanHelpers.perform_all()
+
{:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
+
+ {:ok, _reaction_activity} =
+ CommonAPI.react_with_emoji(activity.id, other_user, ":wow@remote:")
ObanHelpers.perform_all()
@@ -60,11 +163,47 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert %{"id" => id} = json_response_and_validate_schema(result, 200)
assert to_string(activity.id) == id
+ # Remove custom emoji
+
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:")
+
+ assert %{"id" => id} = json_response_and_validate_schema(result, 200)
+ assert to_string(activity.id) == id
+
ObanHelpers.perform_all()
object = Object.get_by_ap_id(activity.data["object"])
- assert object.data["reaction_count"] == 0
+ assert object.data["reaction_count"] == 2
+
+ # Remove custom remote emoji
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
+ |> json_response(200)
+
+ assert result["pleroma"]["emoji_reactions"] == [
+ %{
+ "name" => "wow@remote",
+ "count" => 1,
+ "me" => false,
+ "url" => "https://remote/emoji/wow",
+ "account_ids" => [user.id]
+ }
+ ]
+
+ # Remove custom remote emoji that hasn't been reacted with yet
+ assert conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:zoop@remote:")
+ |> json_response(400)
end
test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
@@ -106,6 +245,38 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
result
end
+ test "GET /api/v1/pleroma/statuses/:id/reactions with legacy format", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ note =
+ insert(:note,
+ user: user,
+ data: %{
+ "reactions" => [["😿", [other_user.ap_id]]]
+ }
+ )
+
+ activity = insert(:note_activity, user: user, note: note)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
+ |> json_response_and_validate_schema(200)
+
+ other_user_id = other_user.id
+
+ assert [
+ %{
+ "name" => "😿",
+ "count" => 1,
+ "me" => false,
+ "url" => nil,
+ "accounts" => [%{"id" => ^other_user_id}]
+ }
+ ] = result
+ end
+
test "GET /api/v1/pleroma/statuses/:id/reactions?with_muted=true", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
@@ -181,7 +352,15 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
- assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] =
+ assert [
+ %{
+ "name" => "🎅",
+ "count" => 1,
+ "accounts" => [represented_user],
+ "me" => false,
+ "url" => nil
+ }
+ ] =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response_and_validate_schema(200)
diff --git a/test/pleroma/web/pleroma_api/views/backup_view_test.exs b/test/pleroma/web/pleroma_api/views/backup_view_test.exs
index a86688bc4..6908463d6 100644
--- a/test/pleroma/web/pleroma_api/views/backup_view_test.exs
+++ b/test/pleroma/web/pleroma_api/views/backup_view_test.exs
@@ -15,4 +15,43 @@ test "it renders the ID" do
result = BackupView.render("show.json", backup: backup)
assert result.id == backup.id
end
+
+ test "it renders the state and processed_number" do
+ user = insert(:user)
+ backup = Backup.new(user)
+
+ result = BackupView.render("show.json", backup: backup)
+ assert result.state == to_string(backup.state)
+ assert result.processed_number == backup.processed_number
+ end
+
+ test "it renders failed state with legacy records" do
+ backup = %Backup{
+ id: 0,
+ content_type: "application/zip",
+ file_name: "dummy",
+ file_size: 1,
+ state: :invalid,
+ processed: true,
+ processed_number: 1,
+ inserted_at: NaiveDateTime.utc_now()
+ }
+
+ result = BackupView.render("show.json", backup: backup)
+ assert result.state == "complete"
+
+ backup = %Backup{
+ id: 0,
+ content_type: "application/zip",
+ file_name: "dummy",
+ file_size: 1,
+ state: :invalid,
+ processed: false,
+ processed_number: 1,
+ inserted_at: NaiveDateTime.utc_now()
+ }
+
+ result = BackupView.render("show.json", backup: backup)
+ assert result.state == "failed"
+ end
end
diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs
index 017c9c5c0..7ab3f5acd 100644
--- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs
+++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs
@@ -24,7 +24,7 @@ test "it displays a chat message" do
filename: "an_image.jpg"
}
- {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+ {:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id)
{:ok, activity} =
CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123")
diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs
index 41fdb93bc..b8acd01c5 100644
--- a/test/pleroma/web/plugs/authentication_plug_test.exs
+++ b/test/pleroma/web/plugs/authentication_plug_test.exs
@@ -70,28 +70,6 @@ test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
assert "$pbkdf2" <> _ = user.password_hash
end
- @tag :skip_on_mac
- test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
- user =
- insert(:user,
- password_hash:
- "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
- )
-
- conn =
- conn
- |> assign(:auth_user, user)
- |> assign(:auth_credentials, %{password: "password"})
- |> AuthenticationPlug.call(%{})
-
- assert conn.assigns.user.id == conn.assigns.auth_user.id
- assert conn.assigns.token == nil
- assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
-
- user = User.get_by_id(user.id)
- assert "$pbkdf2" <> _ = user.password_hash
- end
-
describe "checkpw/2" do
test "check pbkdf2 hash" do
hash =
@@ -101,14 +79,6 @@ test "check pbkdf2 hash" do
refute AuthenticationPlug.checkpw("test-password1", hash)
end
- @tag :skip_on_mac
- test "check sha512-crypt hash" do
- hash =
- "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
-
- assert AuthenticationPlug.checkpw("password", hash)
- end
-
test "check bcrypt hash" do
hash = "$2a$10$uyhC/R/zoE1ndwwCtMusK.TLVzkQ/Ugsbqp3uXI.CTTz0gBw.24jS"
diff --git a/test/pleroma/web/plugs/ensure_privileged_plug_test.exs b/test/pleroma/web/plugs/ensure_privileged_plug_test.exs
new file mode 100644
index 000000000..4b6679b66
--- /dev/null
+++ b/test/pleroma/web/plugs/ensure_privileged_plug_test.exs
@@ -0,0 +1,96 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsurePrivilegedPlugTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Web.Plugs.EnsurePrivilegedPlug
+ import Pleroma.Factory
+
+ test "denies a user that isn't moderator or admin" do
+ clear_config([:instance, :admin_privileges], [])
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> EnsurePrivilegedPlug.call(:cofe)
+
+ assert conn.status == 403
+ end
+
+ test "accepts an admin that is privileged" do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ user = insert(:user, is_admin: true)
+ conn = assign(build_conn(), :user, user)
+
+ ret_conn = EnsurePrivilegedPlug.call(conn, :cofe)
+
+ assert conn == ret_conn
+ end
+
+ test "denies an admin that isn't privileged" do
+ clear_config([:instance, :admin_privileges], [:suya])
+ user = insert(:user, is_admin: true)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> EnsurePrivilegedPlug.call(:cofe)
+
+ assert conn.status == 403
+ end
+
+ test "accepts a moderator that is privileged" do
+ clear_config([:instance, :moderator_privileges], [:cofe])
+ user = insert(:user, is_moderator: true)
+ conn = assign(build_conn(), :user, user)
+
+ ret_conn = EnsurePrivilegedPlug.call(conn, :cofe)
+
+ assert conn == ret_conn
+ end
+
+ test "denies a moderator that isn't privileged" do
+ clear_config([:instance, :moderator_privileges], [:suya])
+ user = insert(:user, is_moderator: true)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> EnsurePrivilegedPlug.call(:cofe)
+
+ assert conn.status == 403
+ end
+
+ test "accepts for a privileged role even if other role isn't privileged" do
+ clear_config([:instance, :admin_privileges], [:cofe])
+ clear_config([:instance, :moderator_privileges], [])
+ user = insert(:user, is_admin: true, is_moderator: true)
+ conn = assign(build_conn(), :user, user)
+
+ ret_conn = EnsurePrivilegedPlug.call(conn, :cofe)
+
+ # privileged through admin role
+ assert conn == ret_conn
+
+ clear_config([:instance, :admin_privileges], [])
+ clear_config([:instance, :moderator_privileges], [:cofe])
+ user = insert(:user, is_admin: true, is_moderator: true)
+ conn = assign(build_conn(), :user, user)
+
+ ret_conn = EnsurePrivilegedPlug.call(conn, :cofe)
+
+ # privileged through moderator role
+ assert conn == ret_conn
+ end
+
+ test "denies when no user is set" do
+ conn =
+ build_conn()
+ |> EnsurePrivilegedPlug.call(:cofe)
+
+ assert conn.status == 403
+ end
+end
diff --git a/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs b/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs
deleted file mode 100644
index c684714b8..000000000
--- a/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs
+++ /dev/null
@@ -1,60 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.EnsureStaffPrivilegedPlugTest do
- use Pleroma.Web.ConnCase, async: true
-
- alias Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug
- import Pleroma.Factory
-
- test "accepts a user that is an admin" do
- user = insert(:user, is_admin: true)
-
- conn = assign(build_conn(), :user, user)
-
- ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{})
-
- assert conn == ret_conn
- end
-
- test "accepts a user that is a moderator when :privileged_staff is enabled" do
- clear_config([:instance, :privileged_staff], true)
- user = insert(:user, is_moderator: true)
-
- conn = assign(build_conn(), :user, user)
-
- ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{})
-
- assert conn == ret_conn
- end
-
- test "denies a user that is a moderator when :privileged_staff is disabled" do
- clear_config([:instance, :privileged_staff], false)
- user = insert(:user, is_moderator: true)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> EnsureStaffPrivilegedPlug.call(%{})
-
- assert conn.status == 403
- end
-
- test "denies a user that isn't a staff member" do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> EnsureStaffPrivilegedPlug.call(%{})
-
- assert conn.status == 403
- end
-
- test "denies when a user isn't set" do
- conn = EnsureStaffPrivilegedPlug.call(build_conn(), %{})
-
- assert conn.status == 403
- end
-end
diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
index ec46b0537..8323ff6ab 100644
--- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs
+++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
@@ -33,11 +33,11 @@ test "does not send Content-Disposition header when name param is not set", %{
test "sends Content-Disposition header when name param is set", %{
attachment_url: attachment_url
} do
- conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif")
+ conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif])
assert Enum.any?(
conn.resp_headers,
- &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""})
+ &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
)
end
end
diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs
index ffdc4e5d7..9064138a6 100644
--- a/test/pleroma/web/rich_media/parser_test.exs
+++ b/test/pleroma/web/rich_media/parser_test.exs
@@ -129,7 +129,7 @@ test "parses twitter card" do
}}
end
- test "parses OEmbed" do
+ test "parses OEmbed and filters HTML tags" do
assert Parser.parse("http://example.com/oembed") ==
{:ok,
%{
@@ -139,7 +139,7 @@ test "parses OEmbed" do
"flickr_type" => "photo",
"height" => "768",
"html" =>
- " ",
+ " ",
"license" => "All Rights Reserved",
"license_id" => 0,
"provider_name" => "Flickr",
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 8b0c84164..d85358fd4 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -22,6 +22,10 @@ defmodule Pleroma.Web.StreamerTest do
setup do: clear_config([:instance, :skip_thread_containment])
describe "get_topic/_ (unauthenticated)" do
+ test "allows no stream" do
+ assert {:ok, nil} = Streamer.get_topic(nil, nil, nil)
+ end
+
test "allows public" do
assert {:ok, "public"} = Streamer.get_topic("public", nil, nil)
assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil)
@@ -29,6 +33,26 @@ test "allows public" do
assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil)
end
+ test "rejects local public streams if restricted_unauthenticated is on" do
+ clear_config([:restrict_unauthenticated, :timelines, :local], true)
+
+ assert {:error, :unauthorized} = Streamer.get_topic("public:local", nil, nil)
+ assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", nil, nil)
+ end
+
+ test "rejects remote public streams if restricted_unauthenticated is on" do
+ clear_config([:restrict_unauthenticated, :timelines, :federated], true)
+
+ assert {:error, :unauthorized} = Streamer.get_topic("public", nil, nil)
+ assert {:error, :unauthorized} = Streamer.get_topic("public:media", nil, nil)
+
+ assert {:error, :unauthorized} =
+ Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"})
+
+ assert {:error, :unauthorized} =
+ Streamer.get_topic("public:remote:media", nil, nil, %{"instance" => "lain.com"})
+ end
+
test "allows instance streams" do
assert {:ok, "public:remote:lain.com"} =
Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"})
@@ -69,6 +93,63 @@ test "allows public streams (regardless of OAuth token scopes)", %{
end
end
+ test "allows local public streams if restricted_unauthenticated is on", %{
+ user: user,
+ token: oauth_token
+ } do
+ clear_config([:restrict_unauthenticated, :timelines, :local], true)
+
+ %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user)
+ %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user)
+
+ assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token)
+
+ assert {:ok, "public:local:media"} =
+ Streamer.get_topic("public:local:media", user, oauth_token)
+
+ for token <- [read_notifications_token, badly_scoped_token] do
+ assert {:error, :unauthorized} = Streamer.get_topic("public:local", user, token)
+
+ assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", user, token)
+ end
+ end
+
+ test "allows remote public streams if restricted_unauthenticated is on", %{
+ user: user,
+ token: oauth_token
+ } do
+ clear_config([:restrict_unauthenticated, :timelines, :federated], true)
+
+ %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user)
+ %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user)
+
+ assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token)
+ assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token)
+
+ assert {:ok, "public:remote:lain.com"} =
+ Streamer.get_topic("public:remote", user, oauth_token, %{"instance" => "lain.com"})
+
+ assert {:ok, "public:remote:media:lain.com"} =
+ Streamer.get_topic("public:remote:media", user, oauth_token, %{
+ "instance" => "lain.com"
+ })
+
+ for token <- [read_notifications_token, badly_scoped_token] do
+ assert {:error, :unauthorized} = Streamer.get_topic("public", user, token)
+ assert {:error, :unauthorized} = Streamer.get_topic("public:media", user, token)
+
+ assert {:error, :unauthorized} =
+ Streamer.get_topic("public:remote", user, token, %{
+ "instance" => "lain.com"
+ })
+
+ assert {:error, :unauthorized} =
+ Streamer.get_topic("public:remote:media", user, token, %{
+ "instance" => "lain.com"
+ })
+ end
+ end
+
test "allows user streams (with proper OAuth token scopes)", %{
user: user,
token: read_oauth_token
@@ -165,7 +246,7 @@ test "it streams the user's post in the 'user' stream", %{user: user, token: oau
Streamer.get_topic_and_add_socket("user", user, oauth_token)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -176,7 +257,7 @@ test "it streams boosts of the user in the 'user' stream", %{user: user, token:
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
{:ok, announce} = CommonAPI.repeat(activity.id, user)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _}
refute Streamer.filtered_by_user?(user, announce)
end
@@ -229,7 +310,7 @@ test "it streams boosts of mastodon user in the 'user' stream", %{
{:ok, %Pleroma.Activity{data: _data, local: false} = announce} =
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _}
refute Streamer.filtered_by_user?(user, announce)
end
@@ -241,7 +322,7 @@ test "it sends notify to in the 'user' stream", %{
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -253,7 +334,7 @@ test "it sends notify to in the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
Streamer.stream("user:notification", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -274,7 +355,12 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{
Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token)
Streamer.stream("user:pleroma_chat", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+ text =
+ StreamerView.render(
+ "chat_update.json",
+ %{chat_message_reference: cm_ref},
+ "user:pleroma_chat:#{user.id}"
+ )
assert text =~ "hey cirno"
assert_receive {:text, ^text}
@@ -292,7 +378,12 @@ test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+ text =
+ StreamerView.render(
+ "chat_update.json",
+ %{chat_message_reference: cm_ref},
+ "user:#{user.id}"
+ )
assert text =~ "hey cirno"
assert_receive {:text, ^text}
@@ -313,7 +404,7 @@ test "it sends chat message notifications to the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
Streamer.stream("user:notification", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -359,7 +450,7 @@ test "it sends favorite to 'user:notification' stream'", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert notif.activity.id == favorite_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -388,7 +479,7 @@ test "it sends follow activities to the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert notif.activity.id == follow_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -453,7 +544,7 @@ test "it streams edits in the 'user' stream", %{user: user, token: oauth_token}
{:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"})
create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
- assert_receive {:render_with_user, _, "status_update.json", ^create}
+ assert_receive {:render_with_user, _, "status_update.json", ^create, _}
refute Streamer.filtered_by_user?(user, edited)
end
@@ -464,7 +555,7 @@ test "it streams own edits in the 'user' stream", %{user: user, token: oauth_tok
{:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"})
create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
- assert_receive {:render_with_user, _, "status_update.json", ^create}
+ assert_receive {:render_with_user, _, "status_update.json", ^create, _}
refute Streamer.filtered_by_user?(user, edited)
end
end
@@ -477,7 +568,7 @@ test "it sends to public (authenticated)" do
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(other_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(other_user, activity)
end
@@ -577,7 +668,7 @@ test "it filters to user if recipients invalid and thread containment is enabled
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -599,7 +690,7 @@ test "it sends message if recipients invalid and thread containment is disabled"
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -622,7 +713,7 @@ test "it sends message if recipients invalid and thread containment is enabled b
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
end
@@ -636,7 +727,7 @@ test "it filters messages involving blocked users", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -653,17 +744,17 @@ test "it filters messages transitively involving blocked users", %{
{:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_one}
+ assert_receive {:render_with_user, _, _, ^activity_one, _}
assert Streamer.filtered_by_user?(blocker, activity_one)
{:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_two}
+ assert_receive {:render_with_user, _, _, ^activity_two, _}
assert Streamer.filtered_by_user?(blocker, activity_two)
{:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_three}
+ assert_receive {:render_with_user, _, _, ^activity_three, _}
assert Streamer.filtered_by_user?(blocker, activity_three)
end
end
@@ -724,7 +815,7 @@ test "it sends wanted private posts to list", %{user: user_a, token: user_a_toke
visibility: "private"
})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user_a, activity)
end
end
@@ -742,7 +833,7 @@ test "it filters muted reblogs", %{user: user1, token: user1_token} do
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, _, ^announce_activity}
+ assert_receive {:render_with_user, _, _, ^announce_activity, _}
assert Streamer.filtered_by_user?(user1, announce_activity)
end
@@ -758,7 +849,7 @@ test "it filters reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert Streamer.filtered_by_user?(user1, notif)
end
@@ -774,7 +865,7 @@ test "it send non-reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
refute Streamer.filtered_by_user?(user1, notif)
end
end
@@ -789,7 +880,7 @@ test "it filters posts from muted threads" do
{:ok, activity} = CommonAPI.post(user, %{status: "super hot take"})
{:ok, _} = CommonAPI.add_mute(user2, activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user2, activity)
end
end
@@ -831,7 +922,7 @@ test "it doesn't send conversation update to the 'direct' stream when the last m
})
create_activity_id = create_activity.id
- assert_receive {:render_with_user, _, _, ^create_activity}
+ assert_receive {:render_with_user, _, _, ^create_activity, _}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
@@ -866,8 +957,8 @@ test "it sends conversation update to the 'direct' stream when a message is dele
visibility: "direct"
})
- assert_receive {:render_with_user, _, _, ^create_activity}
- assert_receive {:render_with_user, _, _, ^create_activity2}
+ assert_receive {:render_with_user, _, _, ^create_activity, _}
+ assert_receive {:render_with_user, _, _, ^create_activity2, _}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
assert_receive {:text, received_conversation1}
@@ -896,7 +987,7 @@ test "it sends conversation update to the 'direct' stream when a message is dele
receive do
{StreamerTest, :ready} ->
- assert_receive {:render_with_user, _, "update.json", _}
+ assert_receive {:render_with_user, _, "update.json", _, _}
receive do
{StreamerTest, :revoked} -> finalize.()
diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs
index fafef54fe..be5e08776 100644
--- a/test/pleroma/web/web_finger_test.exs
+++ b/test/pleroma/web/web_finger_test.exs
@@ -180,5 +180,28 @@ test "respects xml content-type" do
{:ok, _data} = WebFinger.finger("pekorino@pawoo.net")
end
+
+ test "refuses to process XML remote entities" do
+ Tesla.Mock.mock(fn
+ %{
+ url: "https://pawoo.net/.well-known/webfinger?resource=acct:pekorino@pawoo.net"
+ } ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/xml_external_entities.xml"),
+ headers: [{"content-type", "application/xrd+xml"}]
+ }}
+
+ %{url: "https://pawoo.net/.well-known/host-meta"} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta")
+ }}
+ end)
+
+ assert :error = WebFinger.finger("pekorino@pawoo.net")
+ end
end
end
diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs
new file mode 100644
index 000000000..49306430b
--- /dev/null
+++ b/test/pleroma/web/xml_test.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Web.XMLTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Web.XML
+
+ test "refuses to parse any entities from XML" do
+ data = File.read!("test/fixtures/xml_billion_laughs.xml")
+ assert(:error == XML.parse_document(data))
+ end
+
+ test "refuses to load external entities from XML" do
+ data = File.read!("test/fixtures/xml_external_entities.xml")
+ assert(:error == XML.parse_document(data))
+ end
+end
diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs
index 283beee4d..acea0ae00 100644
--- a/test/pleroma/workers/receiver_worker_test.exs
+++ b/test/pleroma/workers/receiver_worker_test.exs
@@ -11,7 +11,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
alias Pleroma.Workers.ReceiverWorker
- test "it ignores MRF reject" do
+ test "it does not retry MRF reject" do
params = insert(:note).data
with_mock Pleroma.Web.ActivityPub.Transmogrifier,
@@ -22,4 +22,31 @@ test "it ignores MRF reject" do
})
end
end
+
+ test "it does not retry ObjectValidator reject" do
+ params =
+ insert(:note_activity).data
+ |> Map.put("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id())
+ |> Map.put("object", %{
+ "type" => "Note",
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id()
+ })
+
+ with_mock Pleroma.Web.ActivityPub.ObjectValidator, [:passthrough],
+ validate: fn _, _ -> {:error, %Ecto.Changeset{}} end do
+ assert {:cancel, {:error, %Ecto.Changeset{}}} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_ap_doc", "params" => params}
+ })
+ end
+ end
+
+ test "it does not retry duplicates" do
+ params = insert(:note_activity).data
+
+ assert {:cancel, :already_present} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_ap_doc", "params" => params}
+ })
+ end
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 09f02458c..d94544717 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -50,7 +50,6 @@ def user_factory(attrs \\ %{}) do
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
- ap_enabled: true,
keys: pem
}
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index b0cf613ac..78a367024 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1380,6 +1380,15 @@ def get("https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
}}
end
+ def get("https://misskey.io/users/83ssedkv53", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get("https://gleasonator.com/users/macgirvin", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1446,6 +1455,15 @@ def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _,
}}
end
+ def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 60a61484f..7727cffbc 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -2,6 +2,8 @@
# Copyright © 2017-2022 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
+Code.put_compiler_option(:warnings_as_errors, true)
+
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude)
diff --git a/tools/check-changelog b/tools/check-changelog
new file mode 100644
index 000000000..d053ed577
--- /dev/null
+++ b/tools/check-changelog
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+echo "looking for change log"
+
+git remote add upstream https://git.pleroma.social/pleroma/pleroma.git
+git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+
+git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \
+ grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\|change\)$'
+ret=$?
+
+if [ $ret -eq 0 ]; then
+ echo "found a changelog entry"
+ exit 0
+else
+ echo "changelog entry not found"
+ exit 1
+fi
diff --git a/tools/collect-changelog b/tools/collect-changelog
new file mode 100755
index 000000000..1e12d5640
--- /dev/null
+++ b/tools/collect-changelog
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+collectType() {
+ local suffix="$1"
+ local header="$2"
+ local printed=0
+ for file in changelog.d/*."$suffix"; do
+ if [ '!' -f "$file" ]; then
+ continue
+ fi
+ if [ "$printed" = 0 ]; then
+ echo
+ echo "### $header"
+ printed=1
+ fi
+ # Normalize any trailing newlines/spaces, etc.
+ echo "- $(cat "$file")"
+ done
+}
+
+collectType security Security
+collectType change Changed
+collectType add Added
+collectType fix Fixed
+collectType remove Removed
+
+rm changelog.d/*