Compare commits
48 Commits
6b882a2c0b
...
1f0ef94271
Author | SHA1 | Date |
---|---|---|
FloatingGhost | 1f0ef94271 | |
floatingghost | 24fe692070 | |
floatingghost | bfcc7404fe | |
r3g_5z | fbfffccc1d | |
r3g_5z | 77174acc7b | |
floatingghost | 59fde45b36 | |
FloatingGhost | 50ee38128b | |
r3g_5z | 90fce918b2 | |
FloatingGhost | 68894089e8 | |
FloatingGhost | a1515f9a60 | |
Weblate | 021b0864a5 | |
Weblate | c33f0065f2 | |
floatingghost | 2144ce5188 | |
FloatingGhost | 739ed14f54 | |
floatingghost | f667884962 | |
FloatingGhost | e49b583147 | |
FloatingGhost | f5a315f04c | |
FloatingGhost | bc265bfd54 | |
FloatingGhost | dcf58a3c53 | |
FloatingGhost | 9db4c2429f | |
FloatingGhost | 6f83ae27aa | |
sn0w | 4c0911592b | |
FloatingGhost | d5828f1c5e | |
FloatingGhost | 0eaec57d3f | |
ilja | 1f863f0a36 | |
ilja | ce517ff4e5 | |
floatingghost | 9addd8f414 | |
Sol Fisher Romanoff | 067bd17e1e | |
floatingghost | 104d8dcc1f | |
floatingghost | 3f1c84d300 | |
FloatingGhost | 4e4bd24813 | |
FloatingGhost | cb3ccf5f47 | |
FloatingGhost | 1afba64464 | |
FloatingGhost | 221a95b860 | |
FloatingGhost | c7369d6d03 | |
sfr | 7c4b415929 | |
floatingghost | 09326ffa56 | |
floatingghost | 4f2f2c9125 | |
ilja | fdf33392b3 | |
FloatingGhost | b058df3faa | |
FloatingGhost | 8e5a88edf7 | |
FloatingGhost | b70a60c6c5 | |
floatingghost | c62e1e3ad5 | |
floatingghost | d55de5debf | |
floatingghost | ec6bf8c3f7 | |
floatingghost | 4c3971aebd | |
floatingghost | 4a94c9a31e | |
timorl | d923cb96b1 |
|
@ -1,10 +1,11 @@
|
|||
*.ex diff=elixir
|
||||
*.exs diff=elixir
|
||||
|
||||
priv/static/instance/static.css diff=css
|
||||
|
||||
# Most of js/css files included in the repo are minified bundles,
|
||||
# and we don't want to search/diff those as text files.
|
||||
*.js binary
|
||||
*.js.map binary
|
||||
*.css binary
|
||||
|
||||
priv/static/instance/static.css diff=css
|
||||
priv/static/static-fe/static-fe.css diff=css
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
name: "Bug report"
|
||||
about: "Something isn't working as expected"
|
||||
title: "[bug] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible.
|
||||
|
||||
# General information
|
||||
- type: dropdown
|
||||
id: installation
|
||||
attributes:
|
||||
label: "Your setup"
|
||||
description: "What sort of installation are you using?"
|
||||
options:
|
||||
- "OTP"
|
||||
- "From source"
|
||||
- "Docker"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: setup-details
|
||||
attributes:
|
||||
label: "Extra details"
|
||||
description: "If installing from source or docker, please specify your distro or docker setup."
|
||||
placeholder: "e.g. Alpine Linux edge"
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: "Which version of Akkoma are you running? If running develop, specify the commit hash."
|
||||
placeholder: "e.g. 2022.11, 4e4bd248"
|
||||
- type: input
|
||||
id: postgres
|
||||
attributes:
|
||||
label: "PostgreSQL version"
|
||||
placeholder: "14"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# The issue"
|
||||
- type: textarea
|
||||
id: attempt
|
||||
attributes:
|
||||
label: "What were you trying to do?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reality
|
||||
attributes:
|
||||
label: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: "Logs"
|
||||
description: "Please copy and paste any relevant log output, if applicable."
|
||||
render: shell
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: "Severity"
|
||||
description: "Does this issue prevent you from using the software as normal?"
|
||||
options:
|
||||
- "I cannot use the software"
|
||||
- "I cannot use it as easily as I'd like"
|
||||
- "I can manage"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this issue?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/akkoma/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this issue mentioned anywhere."
|
|
@ -0,0 +1,29 @@
|
|||
name: "Feature request"
|
||||
about: "I'd like something to be added to Akkoma"
|
||||
title: "[feat] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: "The idea"
|
||||
description: "What do you think you should be able to do in Akkoma?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: "The reasoning"
|
||||
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this feature request?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/akkoma/issues), or the one for [pleroma-fe](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this feature request mentioned anywhere."
|
||||
- label: "This feature is related to the Akkoma backend specifically, and not pleroma-fe."
|
11
CHANGELOG.md
|
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
## 2022.12
|
||||
|
||||
## Added
|
||||
- Config: HTTP timeout options, :pool\_timeout and :receive\_timeout
|
||||
|
@ -12,6 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Ability to set a default post expiry time, after which the post will be deleted. If used in concert with ActivityExpiration MRF, the expiry which comes _sooner_ will be applied.
|
||||
- Regular task to prune local transient activities
|
||||
- Task to manually run the transient prune job (pleroma.database prune\_task)
|
||||
- Ability to follow hashtags
|
||||
- Option to extend `reject` in MRF-Simple to apply to entire threads, where the originating instance is rejected
|
||||
- Extra information to failed HTTP requests
|
||||
|
||||
## Changed
|
||||
- MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py)
|
||||
|
@ -19,6 +22,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- NormalizeMarkup MRF is now on by default
|
||||
- Follow/Block/Mute imports now spin off into *n* tasks to avoid the oban timeout
|
||||
- Transient activities recieved from remote servers are no longer persisted in the database
|
||||
- Overhauled static-fe view for logged-out users
|
||||
- Blocked instances will now not be sent _any_ requests, even fetch ones that would get rejected by MRF anyhow
|
||||
|
||||
## Removed
|
||||
- FollowBotPolicy
|
||||
- Passing of undo/block into MRF
|
||||
|
||||
## Upgrade Notes
|
||||
- If you have an old instance, you will probably want to run `mix pleroma.database prune_task` in the foreground to catch it up with the history of your instance.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
FROM hexpm/elixir:1.13.4-erlang-24.3.4.5-alpine-3.15.6
|
||||
|
||||
ENV MIX_ENV=prod
|
||||
ENV ERL_EPMD_ADDRESS=127.0.0.1
|
||||
|
||||
ARG HOME=/opt/akkoma
|
||||
|
||||
|
|
|
@ -391,7 +391,8 @@ config :pleroma, :mrf_simple,
|
|||
accept: [],
|
||||
avatar_removal: [],
|
||||
banner_removal: [],
|
||||
reject_deletes: []
|
||||
reject_deletes: [],
|
||||
handle_threads: true
|
||||
|
||||
config :pleroma, :mrf_keyword,
|
||||
reject: [],
|
||||
|
|
|
@ -723,7 +723,8 @@ config :pleroma, :config_description, [
|
|||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode"
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
@ -1294,7 +1295,13 @@ config :pleroma, :config_description, [
|
|||
label: "Post Content Type",
|
||||
type: {:dropdown, :atom},
|
||||
description: "Default post formatting option",
|
||||
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
|
||||
suggestions: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :redirectRootNoLogin,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
MIX_ENV=prod
|
||||
ERL_EPMD_ADDRESS=127.0.0.1
|
||||
DB_NAME=akkoma
|
||||
DB_USER=akkoma
|
||||
DB_PASS=akkoma
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
|
||||
"sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2022.9.24"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
|
@ -27,7 +27,7 @@
|
|||
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
||||
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"click": {
|
||||
|
@ -66,15 +66,16 @@
|
|||
"sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874",
|
||||
"sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==3.3.7"
|
||||
},
|
||||
"markdown-include": {
|
||||
"hashes": [
|
||||
"sha256:a06183b7c7225e73112737acdc6fe0ac0686c39457234eeb5ede23881fed001d"
|
||||
"sha256:b8f6b6f4e8b506cbe773d7e26c74a97d1354c35f3a3452d3449140a8f578d665",
|
||||
"sha256:d12fb51500c46334a53608635035c78b7d8ad7f772566f70b8a6a9b2ef2ddbf5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
|
@ -127,7 +128,7 @@
|
|||
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
|
||||
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"mkdocs": {
|
||||
|
@ -140,26 +141,26 @@
|
|||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:143ea55843b3747b640e1110824d91e8a4c670352380e166e64959f9abe98862",
|
||||
"sha256:45eeabb23d2caba8fa3b85c91d9ec8e8b22add716e9bba8faf16d56af8aa5622"
|
||||
"sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7",
|
||||
"sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.5.9"
|
||||
"version": "==8.5.11"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
"sha256:96ca979dae66d65c2099eefe189b49d5ac62f76afb59c38e069ffc7cf3c131ec",
|
||||
"sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"
|
||||
"sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93",
|
||||
"sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1"
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pygments": {
|
||||
|
@ -167,16 +168,16 @@
|
|||
"sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1",
|
||||
"sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2.13.0"
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1bd4a173095ef8c433b831af1f3cb13c10883be0c100ae613560668e594651f7",
|
||||
"sha256:8e62688a8b1128acd42fa823f3d429d22f4284b5e6dd4d3cd56721559a5a211b"
|
||||
"sha256:0f8fb7b74a37a61cc34e90b2c91865458b713ec774894ffad64353a5fce85cfc",
|
||||
"sha256:ac698c15265680db5eb13cd4342abfcde2079ac01e5486028f47a1b41547b859"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==9.8"
|
||||
"version": "==9.9"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
|
@ -237,7 +238,7 @@
|
|||
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"pyyaml-env-tag": {
|
||||
|
@ -245,7 +246,7 @@
|
|||
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
|
||||
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==0.1"
|
||||
},
|
||||
"requests": {
|
||||
|
@ -266,42 +267,45 @@
|
|||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
|
||||
"sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
|
||||
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
|
||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
|
||||
"version": "==1.26.12"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.26.13"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
"sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412",
|
||||
"sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654",
|
||||
"sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306",
|
||||
"sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33",
|
||||
"sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd",
|
||||
"sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7",
|
||||
"sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892",
|
||||
"sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609",
|
||||
"sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6",
|
||||
"sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1",
|
||||
"sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591",
|
||||
"sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d",
|
||||
"sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d",
|
||||
"sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c",
|
||||
"sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3",
|
||||
"sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39",
|
||||
"sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213",
|
||||
"sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330",
|
||||
"sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428",
|
||||
"sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1",
|
||||
"sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846",
|
||||
"sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153",
|
||||
"sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3",
|
||||
"sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9",
|
||||
"sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"
|
||||
"sha256:1893d425ef4fb4f129ee8ef72226836619c2950dd0559bba022b0818c63a7b60",
|
||||
"sha256:1a410dd4d0adcc86b4c71d1317ba2ea2c92babaf5b83321e4bde2514525544d5",
|
||||
"sha256:1f2b0665c57358ce9786f06f5475bc083fea9d81ecc0efa4733fd0c320940a37",
|
||||
"sha256:1f8eca9d294a4f194ce9df0d97d19b5598f310950d3ac3dd6e8d25ae456d4c8a",
|
||||
"sha256:27e49268735b3c27310883012ab3bd86ea0a96dcab90fe3feb682472e30c90f3",
|
||||
"sha256:28704c71afdb79c3f215c90231e41c52b056ea880b6be6cee035c6149d658ed1",
|
||||
"sha256:2ac0bd7c206bb6df78ef9e8ad27cc1346f2b41b1fef610395607319cdab89bc1",
|
||||
"sha256:2af1a29fd14fc0a87fb6ed762d3e1ae5694dcde22372eebba50e9e5be47af03c",
|
||||
"sha256:3a048865c828389cb06c0bebf8a883cec3ae58ad3e366bcc38c61d8455a3138f",
|
||||
"sha256:441024df19253bb108d3a8a5de7a186003d68564084576fecf7333a441271ef7",
|
||||
"sha256:56fb3f40fc3deecf6e518303c7533f5e2a722e377b12507f6de891583f1b48aa",
|
||||
"sha256:619d63fa5be69f89ff3a93e165e602c08ed8da402ca42b99cd59a8ec115673e1",
|
||||
"sha256:74535e955359d79d126885e642d3683616e6d9ab3aae0e7dcccd043bd5a3ff4f",
|
||||
"sha256:76a2743402b794629a955d96ea2e240bd0e903aa26e02e93cd2d57b33900962b",
|
||||
"sha256:83cf8bc60d9c613b66a4c018051873d6273d9e45d040eed06d6a96241bd8ec01",
|
||||
"sha256:920a4bda7daa47545c3201a3292e99300ba81ca26b7569575bd086c865889090",
|
||||
"sha256:9e99c1713e4436d2563f5828c8910e5ff25abd6ce999e75f15c15d81d41980b6",
|
||||
"sha256:a5bd9e8656d07cae89ac464ee4bcb6f1b9cecbedc3bf1334683bed3d5afd39ba",
|
||||
"sha256:ad0150536469fa4b693531e497ffe220d5b6cd76ad2eda474a5e641ee204bbb6",
|
||||
"sha256:af4b5c7ba60206759a1d99811b5938ca666ea9562a1052b410637bb96ff97512",
|
||||
"sha256:c7bd98813d34bfa9b464cf8122e7d4bec0a5a427399094d2c17dd5f70d59bc61",
|
||||
"sha256:ceaa9268d81205876bedb1069f9feab3eccddd4b90d9a45d06a0df592a04cae9",
|
||||
"sha256:cf05e6ff677b9655c6e9511d02e9cc55e730c4e430b7a54af9c28912294605a4",
|
||||
"sha256:d0fb5f2b513556c2abb578c1066f5f467d729f2eb689bc2db0739daf81c6bb7e",
|
||||
"sha256:d6ae890798a3560688b441ef086bb66e87af6b400a92749a18b856a134fc0318",
|
||||
"sha256:e5aed2a700a18c194c39c266900d41f3db0c1ebe6b8a0834b9995c835d2ca66e",
|
||||
"sha256:e722755d995035dd32177a9c633d158f2ec604f2a358b545bba5bed53ab25bca",
|
||||
"sha256:ed91c3ccfc23398e7aa9715abf679d5c163394b8cad994f34f156d57a7c163dc"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.1.9"
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Diagnostics
|
||||
|
||||
A few tasks to help with debugging, troubleshooting, and diagnosing problems.
|
||||
|
||||
They mostly relate to common postgres queries.
|
||||
|
||||
## Home timeline query plan
|
||||
|
||||
This task will print a query plan for the home timeline of a given user.
|
||||
|
||||
=== "OTP"
|
||||
|
||||
`./bin/pleroma_ctl diagnostics home_timeline <nickname>`
|
||||
|
||||
=== "From Source"
|
||||
|
||||
`mix pleroma.diagnostics home_timeline <nickname>`
|
||||
|
||||
## User timeline query plan
|
||||
|
||||
This task will print a query plan for the user timeline of a given user,
|
||||
from the perspective of another given user.
|
||||
|
||||
=== "OTP"
|
||||
|
||||
`./bin/pleroma_ctl diagnostics user_timeline <nickname> <viewing_nickname>`
|
||||
|
||||
=== "From Source"
|
||||
|
||||
`mix pleroma.diagnostics user_timeline <nickname> <viewing_nickname>`
|
|
@ -4,38 +4,62 @@
|
|||
|
||||
1. Stop the Akkoma service.
|
||||
2. Go to the working directory of Akkoma (default is `/opt/akkoma`)
|
||||
3. Run `sudo -Hu postgres pg_dump -d <akkoma_db> --format=custom -f </path/to/backup_location/akkoma.pgdump>` (make sure the postgres user has write access to the destination file)
|
||||
4. Copy `akkoma.pgdump`, `config/prod.secret.exs`, `config/setup_db.psql` (if still available) and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too.
|
||||
3. Run[¹] `sudo -Hu postgres pg_dump -d akkoma --format=custom -f </path/to/backup_location/akkoma.pgdump>` (make sure the postgres user has write access to the destination file)
|
||||
4. Copy `akkoma.pgdump`, `config/prod.secret.exs`[²], `config/setup_db.psql` (if still available) and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too.
|
||||
5. Restart the Akkoma service.
|
||||
|
||||
[¹]: We assume the database name is "akkoma". If not, you can find the correct name in your config files.
|
||||
[²]: If you've installed using OTP, you need `config/config.exs` instead of `config/prod.secret.exs`.
|
||||
|
||||
## Restore/Move
|
||||
|
||||
1. Optionally reinstall Akkoma (either on the same server or on another server if you want to move servers).
|
||||
2. Stop the Akkoma service.
|
||||
3. Go to the working directory of Akkoma (default is `/opt/akkoma`)
|
||||
4. Copy the above mentioned files back to their original position.
|
||||
5. Drop the existing database and user if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE <akkoma_db>;';` `sudo -Hu postgres psql -c 'DROP USER <akkoma_db>;'`
|
||||
6. Restore the database schema and akkoma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`.
|
||||
|
||||
Alternatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the akkoma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed.
|
||||
|
||||
7. Now restore the Akkoma instance's data into the empty database schema: `sudo -Hu postgres pg_restore -d <akkoma_db> -v -1 </path/to/backup_location/akkoma.pgdump>`
|
||||
8. If you installed a newer Akkoma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
|
||||
5. Drop the existing database and user if restoring in-place[¹]. `sudo -Hu postgres psql -c 'DROP DATABASE akkoma;';` `sudo -Hu postgres psql -c 'DROP USER akkoma;'`
|
||||
6. Restore the database schema and akkoma role using either of the following options
|
||||
* You can use the original `setup_db.psql` if you have it[²]: `sudo -Hu postgres psql -f config/setup_db.psql`.
|
||||
* Or recreate the database and user yourself (replace the password with the one you find in the config file) `sudo -Hu postgres psql -c "CREATE USER akkoma WITH ENCRYPTED PASSWORD '<database-password-wich-you-can-find-in-your-config-file>'; CREATE DATABASE akkoma OWNER akkoma;"`.
|
||||
7. Now restore the Akkoma instance's data into the empty database schema[¹][³]: `sudo -Hu postgres pg_restore -d akkoma -v -1 </path/to/backup_location/akkoma.pgdump>`
|
||||
8. If you installed a newer Akkoma version, you should run `MIX_ENV=prod mix ecto.migrate`[⁴]. This task performs database migrations, if there were any.
|
||||
9. Restart the Akkoma service.
|
||||
10. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries.
|
||||
11. If setting up on a new server configure Nginx by using the `installation/akkoma.nginx` config sample or reference the Akkoma installation guide for your OS which contains the Nginx configuration instructions.
|
||||
|
||||
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
|
||||
[¹]: We assume the database name and user are both "akkoma". If not, you can find the correct name in your config files.
|
||||
[²]: You can recreate the `config/setup_db.psql` by running the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backed up config file. This will also create a new `config/generated_config.exs` file which you may delete as it is not needed.
|
||||
[³]: `pg_restore` will add data before adding indexes. The indexes are added in alphabetical order. There's one index, `activities_visibility_index` which may take a long time because it can't make use of an index that's only added later. You can significantly speed up restoration by skipping this index and add it afterwards. For that, you can do the following (we assume the akkoma.pgdump is in the directory you're running the commands):
|
||||
|
||||
```sh
|
||||
pg_restore -l akkoma.pgdump > db.list
|
||||
|
||||
# Comment out the step for creating activities_visibility_index by adding a semi colon at the start of the line
|
||||
sed -i -E 's/(.*activities_visibility_index.*)/;\1/' db.list
|
||||
|
||||
# We restore the database using the db.list list-file
|
||||
sudo -Hu postgres pg_restore -L db.list -d akkoma -v -1 akkoma.pgdump
|
||||
|
||||
# You can see the sql statement with which to create the index using
|
||||
grep -Eao 'CREATE INDEX activities_visibility_index.*' akkoma.pgdump
|
||||
|
||||
# Then create the index manually
|
||||
# Make sure that the command to create is correct! You never know it has changed since writing this guide
|
||||
sudo -Hu postgres psql -d pleroma_ynh -c "CREATE INDEX activities_visibility_index ON public.activities USING btree (public.activity_visibility(actor, recipients, data), id DESC NULLS LAST) WHERE ((data ->> 'type'::text) = 'Create'::text);"
|
||||
```
|
||||
[⁴]: Prefix with `MIX_ENV=prod` to run it using the production config file.
|
||||
|
||||
## Remove
|
||||
|
||||
1. Optionally you can remove the users of your instance. This will trigger delete requests for their accounts and posts. Note that this is 'best effort' and doesn't mean that all traces of your instance will be gone from the fediverse.
|
||||
* You can do this from the admin-FE where you can select all local users and delete the accounts using the *Moderate multiple users* dropdown.
|
||||
* You can also list local users and delete them individualy using the CLI tasks for [Managing users](./CLI_tasks/user.md).
|
||||
* You can also list local users and delete them individually using the CLI tasks for [Managing users](./CLI_tasks/user.md).
|
||||
2. Stop the Akkoma service `systemctl stop akkoma`
|
||||
3. Disable akkoma from systemd `systemctl disable akkoma`
|
||||
3. Disable Akkoma from systemd `systemctl disable akkoma`
|
||||
4. Remove the files and folders you created during installation (see installation guide). This includes the akkoma, nginx and systemd files and folders.
|
||||
5. Reload nginx now that the configuration is removed `systemctl reload nginx`
|
||||
6. Remove the database and database user `sudo -Hu postgres psql -c 'DROP DATABASE <akkoma_db>;';` `sudo -Hu postgres psql -c 'DROP USER <akkoma_db>;'`
|
||||
6. Remove the database and database user[¹] `sudo -Hu postgres psql -c 'DROP DATABASE akkoma;';` `sudo -Hu postgres psql -c 'DROP USER akkoma;'`
|
||||
7. Remove the system user `userdel akkoma`
|
||||
8. Remove the dependencies that you don't need anymore (see installation guide). Make sure you don't remove packages that are still needed for other software that you have running!
|
||||
|
||||
[¹]: We assume the database name and user are both "akkoma". If not, you can find the correct name in your config files.
|
||||
|
|
|
@ -221,11 +221,6 @@ Notes:
|
|||
- The hashtags in the configuration do not have a leading `#`.
|
||||
- This MRF Policy is always enabled, if you want to disable it you have to set empty lists
|
||||
|
||||
#### :mrf_follow_bot
|
||||
|
||||
* `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested.
|
||||
|
||||
|
||||
### :activitypub
|
||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# Using a Varnish Cache
|
||||
|
||||
Varnish is a layer that sits between your web server and your backend application -
|
||||
it does something similar to nginx caching, but tends to be optimised for speed over
|
||||
all else.
|
||||
|
||||
To set up a varnish cache, first you'll need to install varnish.
|
||||
|
||||
This will vary by distribution, and since this is a rather advanced guide,
|
||||
no copy-paste instructions are provided. It's probably in your distribution's
|
||||
package manager, though. `apt-get install varnish` and so on.
|
||||
|
||||
Once you have varnish installed, you'll need to configure it to work with akkoma.
|
||||
|
||||
Copy the configuration file to the varnish configuration directory:
|
||||
|
||||
cp installation/akkoma.vcl /etc/varnish/akkoma.vcl
|
||||
|
||||
You may want to check if varnish added a `default.vcl` file to the same directory,
|
||||
if so you can just remove it without issue.
|
||||
|
||||
Then boot up varnish, probably `systemctl start varnish` or `service varnish start`.
|
||||
|
||||
Now you should be able to `curl -D- localhost:6081` and see a bunch of
|
||||
akkoma javascript.
|
||||
|
||||
Once that's out of the way, we can point our webserver at varnish. This
|
||||
|
||||
=== "Nginx"
|
||||
|
||||
upstream phoenix {
|
||||
server 127.0.0.1:6081 max_fails=5 fail_timeout=60s;
|
||||
}
|
||||
|
||||
|
||||
=== "Caddy"
|
||||
|
||||
reverse_proxy 127.0.0.1:6081
|
||||
|
||||
Now hopefully it all works
|
||||
|
||||
If you get a HTTPS redirect loop, you may need to remove this part of the VCL
|
||||
|
||||
```vcl
|
||||
if (std.port(server.ip) != 443) {
|
||||
set req.http.X-Forwarded-Proto = "http";
|
||||
set req.http.x-redir = "https://" + req.http.host + req.url;
|
||||
return (synth(750, ""));
|
||||
} else {
|
||||
set req.http.X-Forwarded-Proto = "https";
|
||||
}
|
||||
```
|
||||
|
||||
This will allow your webserver alone to handle redirects.
|
|
@ -100,3 +100,12 @@ the `frontends` directory.
|
|||
## Styling rendered pages
|
||||
|
||||
To overwrite the CSS stylesheet of the OAuth form and other static pages, you can upload your own CSS file to `instance/static/static.css`. This will completely replace the CSS used by those pages, so it might be a good idea to copy the one from `priv/static/instance/static.css` and make your changes.
|
||||
|
||||
## Overriding pleroma-fe styles
|
||||
|
||||
To overwrite the CSS stylesheet of pleroma-fe, you can put a file at
|
||||
`$static_dir/static/custom.css` containing your styles. These will be loaded
|
||||
with the rest of the CSS.
|
||||
|
||||
You will probably have to put `!important` on most/all your styles to override the
|
||||
default ones, due to the specificity precedence of CSS.
|
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -18,6 +18,12 @@ dev-db/postgresql uuid
|
|||
|
||||
You could opt to add `USE="uuid"` to `/etc/portage/make.conf` if you'd rather set this as a global USE flags, but this flags does unrelated things in other packages, so keep that in mind if you elect to do so.
|
||||
|
||||
If you are planning to use `nginx`, as this guide suggests, you should also add the following flag to the same file.
|
||||
|
||||
```text
|
||||
www-servers/nginx NGINX_MODULES_HTTP: slice
|
||||
```
|
||||
|
||||
Double check your compiler flags in `/etc/portage/make.conf`. If you require any special compilation flags or would like to set up remote builds, now is the time to do so. Be sure that your CFLAGS and MAKEOPTS make sense for the platform you are using. It is not recommended to use above `-O2` or risky optimization flags for a production server.
|
||||
|
||||
### Installing a cron daemon
|
||||
|
@ -262,7 +268,7 @@ Even if you are using S3, Akkoma needs someplace to store media posted on your i
|
|||
|
||||
```shell
|
||||
akkoma$ mkdir -p ~/akkoma/uploads
|
||||
```
|
||||
```
|
||||
|
||||
#### init.d service
|
||||
|
||||
|
@ -272,7 +278,9 @@ Even if you are using S3, Akkoma needs someplace to store media posted on your i
|
|||
# cp /home/akkoma/akkoma/installation/init.d/akkoma /etc/init.d/
|
||||
```
|
||||
|
||||
* Be sure to take a look at this service file and make sure that all paths fit your installation
|
||||
* Change the `/opt/akkoma` path in this file to `/home/akkoma/akkoma`
|
||||
|
||||
* Be sure to take a look at this service file and make sure that all other paths fit your installation
|
||||
|
||||
* Enable and start `akkoma`:
|
||||
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
site_name: Akkoma Documentation
|
||||
theme:
|
||||
favicon: 'images/akko_badday.png'
|
||||
favicon: 'images/favicon.ico'
|
||||
name: 'material'
|
||||
custom_dir: 'theme'
|
||||
# Disable google fonts
|
||||
font: false
|
||||
logo: 'images/akko_badday.png'
|
||||
logo: 'images/logo.png'
|
||||
features:
|
||||
- tabs
|
||||
- navigation.tabs
|
||||
- toc.follow
|
||||
- navigation.instant
|
||||
- navigation.sections
|
||||
palette:
|
||||
primary: 'deep purple'
|
||||
accent: 'blue grey'
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
primary: 'deep purple'
|
||||
accent: 'blue grey'
|
||||
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
primary: 'deep purple'
|
||||
accent: 'blue grey'
|
||||
|
||||
extra_css:
|
||||
- css/extra.css
|
||||
|
@ -31,7 +47,8 @@ markdown_extensions:
|
|||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.details
|
||||
- markdown_include.include:
|
||||
base_path: docs
|
||||
|
|
|
@ -4,7 +4,6 @@ After=network.target postgresql.service
|
|||
|
||||
[Service]
|
||||
ExecReload=/bin/kill $MAINPID
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
; Uncomment this if you're on Arch Linux
|
||||
|
@ -15,6 +14,9 @@ User=akkoma
|
|||
; Declares that Akkoma runs in production mode.
|
||||
Environment="MIX_ENV=prod"
|
||||
|
||||
; Don't listen epmd on 0.0.0.0
|
||||
Environment="ERL_EPMD_ADDRESS=127.0.0.1"
|
||||
|
||||
; Make sure that all paths fit your installation.
|
||||
; Path to the home directory of the user running the Akkoma service.
|
||||
Environment="HOME=/var/lib/akkoma"
|
||||
|
|
|
@ -12,7 +12,8 @@ environment =
|
|||
HOME=/home/akkoma,
|
||||
USER=akkoma,
|
||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/home/akkoma/bin:%(ENV_PATH)s",
|
||||
PWD=/home/akkoma/akkoma
|
||||
PWD=/home/akkoma/akkoma,
|
||||
ERL_EPMD_ADDRESS=127.0.0.1
|
||||
stdout_logfile=/home/akkoma/logs/stdout.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=10
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Recommended varnishncsa logging format: '%h %l %u %t "%m %{X-Forwarded-Proto}i://%{Host}i%U%q %H" %s %b "%{Referer}i" "%{User-agent}i"'
|
||||
# Please use Varnish 7.0+ for proper Range Requests / Chunked encoding support
|
||||
vcl 4.1;
|
||||
import std;
|
||||
|
||||
|
@ -22,11 +23,6 @@ sub vcl_recv {
|
|||
set req.http.X-Forwarded-Proto = "https";
|
||||
}
|
||||
|
||||
# CHUNKED SUPPORT
|
||||
if (req.http.Range ~ "bytes=") {
|
||||
set req.http.x-range = req.http.Range;
|
||||
}
|
||||
|
||||
# Pipe if WebSockets request is coming through
|
||||
if (req.http.upgrade ~ "(?i)websocket") {
|
||||
return (pipe);
|
||||
|
@ -35,9 +31,9 @@ sub vcl_recv {
|
|||
# Allow purging of the cache
|
||||
if (req.method == "PURGE") {
|
||||
if (!client.ip ~ purge) {
|
||||
return(synth(405,"Not allowed."));
|
||||
return (synth(405,"Not allowed."));
|
||||
}
|
||||
return(purge);
|
||||
return (purge);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,17 +49,11 @@ sub vcl_backend_response {
|
|||
return (retry);
|
||||
}
|
||||
|
||||
# CHUNKED SUPPORT
|
||||
if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) {
|
||||
set beresp.ttl = 10m;
|
||||
set beresp.http.CR = beresp.http.content-range;
|
||||
}
|
||||
|
||||
# Bypass cache for large files
|
||||
# 50000000 ~ 50MB
|
||||
if (std.integer(beresp.http.content-length, 0) > 50000000) {
|
||||
set beresp.uncacheable = true;
|
||||
return(deliver);
|
||||
return (deliver);
|
||||
}
|
||||
|
||||
# Don't cache objects that require authentication
|
||||
|
@ -94,7 +84,7 @@ sub vcl_synth {
|
|||
if (resp.status == 750) {
|
||||
set resp.status = 301;
|
||||
set resp.http.Location = req.http.x-redir;
|
||||
return(deliver);
|
||||
return (deliver);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,25 +96,12 @@ sub vcl_pipe {
|
|||
}
|
||||
}
|
||||
|
||||
sub vcl_hash {
|
||||
# CHUNKED SUPPORT
|
||||
if (req.http.x-range ~ "bytes=") {
|
||||
hash_data(req.http.x-range);
|
||||
unset req.http.Range;
|
||||
}
|
||||
}
|
||||
|
||||
sub vcl_backend_fetch {
|
||||
# Be more lenient for slow servers on the fediverse
|
||||
if (bereq.url ~ "^/proxy/") {
|
||||
set bereq.first_byte_timeout = 300s;
|
||||
}
|
||||
|
||||
# CHUNKED SUPPORT
|
||||
if (bereq.http.x-range) {
|
||||
set bereq.http.Range = bereq.http.x-range;
|
||||
}
|
||||
|
||||
if (bereq.retries == 0) {
|
||||
# Clean up the X-Varnish-Backend-503 flag that is used internally
|
||||
# to mark broken backend responses that should be retried.
|
||||
|
@ -143,14 +120,6 @@ sub vcl_backend_fetch {
|
|||
}
|
||||
}
|
||||
|
||||
sub vcl_deliver {
|
||||
# CHUNKED SUPPORT
|
||||
if (resp.http.CR) {
|
||||
set resp.http.Content-Range = resp.http.CR;
|
||||
unset resp.http.CR;
|
||||
}
|
||||
}
|
||||
|
||||
sub vcl_backend_error {
|
||||
# Retry broken backend responses.
|
||||
set bereq.http.X-Varnish-Backend-503 = "1";
|
||||
|
|
|
@ -18,7 +18,8 @@ load_rc_config ${name}
|
|||
: ${akkoma_user:=akkoma}
|
||||
: ${akkoma_home:=$(getent passwd ${akkoma_user} | awk -F: '{print $6}')}
|
||||
: ${akkoma_chdir:="${akkoma_home}/akkoma"}
|
||||
: ${akkoma_env:="HOME=${akkoma_home} MIX_ENV=prod"}
|
||||
: ${akkoma_env:="HOME=${akkoma_home} MIX_ENV=prod ERL_EPMD_ADDRESS=127.0.0.1"}
|
||||
|
||||
|
||||
command=/usr/local/bin/elixir
|
||||
command_args="--erl \"-detached\" -S /usr/local/bin/mix phx.server"
|
||||
|
|
|
@ -31,6 +31,7 @@ else
|
|||
fi
|
||||
|
||||
export MIX_ENV=prod
|
||||
export ERL_EPMD_ADDRESS=127.0.0.1
|
||||
|
||||
depend() {
|
||||
need nginx postgresql
|
||||
|
|
|
@ -14,7 +14,7 @@ start_precmd="ulimit -n unlimited"
|
|||
pidfile="/dev/null"
|
||||
|
||||
akkoma_chdir="${akkoma_home}/akkoma"
|
||||
akkoma_env="HOME=${akkoma_home} MIX_ENV=prod"
|
||||
akkoma_env="HOME=${akkoma_home} MIX_ENV=prod ERL_EPMD_ADDRESS=127.0.0.1"
|
||||
|
||||
check_pidfile()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
defmodule Mix.Tasks.Pleroma.Diagnostics do
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
import Mix.Pleroma
|
||||
import Ecto.Query
|
||||
use Mix.Task
|
||||
|
||||
def run(["home_timeline", nickname]) do
|
||||
start_pleroma()
|
||||
user = Repo.get_by!(User, nickname: nickname)
|
||||
Logger.info("Home timeline query #{user.nickname}")
|
||||
|
||||
followed_hashtags =
|
||||
user
|
||||
|> User.followed_hashtags()
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
params =
|
||||
%{limit: 20}
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> Map.put(:announce_filtering_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:followed_hashtags, followed_hashtags)
|
||||
|> Map.delete(:local)
|
||||
|
||||
list_memberships = Pleroma.List.memberships(user)
|
||||
recipients = [user.ap_id | User.following(user)]
|
||||
|
||||
query =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(
|
||||
recipients ++ list_memberships,
|
||||
params
|
||||
)
|
||||
|> limit(20)
|
||||
|
||||
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
def run(["user_timeline", nickname, reading_nickname]) do
|
||||
start_pleroma()
|
||||
user = Repo.get_by!(User, nickname: nickname)
|
||||
reading_user = Repo.get_by!(User, nickname: reading_nickname)
|
||||
Logger.info("User timeline query #{user.nickname}")
|
||||
|
||||
params =
|
||||
%{limit: 20}
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|> Map.put(:user, reading_user)
|
||||
|> Map.put(:actor_id, user.ap_id)
|
||||
|> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))
|
||||
|
||||
list_memberships = Pleroma.List.memberships(user)
|
||||
|
||||
recipients =
|
||||
%{
|
||||
godmode: params[:godmode],
|
||||
reading_user: reading_user
|
||||
}
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.user_activities_recipients()
|
||||
|
||||
query =
|
||||
(recipients ++ list_memberships)
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(params)
|
||||
|> limit(20)
|
||||
|
||||
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
|
||||
|> IO.puts()
|
||||
end
|
||||
end
|
|
@ -471,9 +471,15 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
|
||||
def run(["timeline_query", nickname]) do
|
||||
start_pleroma()
|
||||
|
||||
params = %{local: true}
|
||||
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
followed_hashtags =
|
||||
user
|
||||
|> User.followed_hashtags()
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|
@ -484,6 +490,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
|> Map.put(:announce_filtering_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:local_only, params[:local])
|
||||
|> Map.put(:hashtags, followed_hashtags)
|
||||
|> Map.delete(:local)
|
||||
|
||||
_activities =
|
||||
|
|
|
@ -68,7 +68,7 @@ defmodule Akkoma.Collections.Fetcher do
|
|||
items
|
||||
end
|
||||
else
|
||||
{:error, "Object has been deleted"} ->
|
||||
{:error, {"Object has been deleted", _, _}} ->
|
||||
items
|
||||
|
||||
{:error, error} ->
|
||||
|
|
|
@ -25,7 +25,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
def check_simple_policy_tuples do
|
||||
has_strings =
|
||||
Config.get([:mrf_simple])
|
||||
|> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end)
|
||||
|> Enum.any?(fn {_, v} -> is_list(v) and Enum.any?(v, &is_binary/1) end)
|
||||
|
||||
if has_strings do
|
||||
Logger.warn("""
|
||||
|
@ -66,6 +66,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
|
||||
new_config =
|
||||
Config.get([:mrf_simple])
|
||||
|> Enum.filter(fn {_k, v} -> not is_atom(v) end)
|
||||
|> Enum.map(fn {k, v} ->
|
||||
{k,
|
||||
Enum.map(v, fn
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Hashtag do
|
|||
|
||||
alias Ecto.Multi
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.User.HashtagFollow
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
|
@ -27,6 +28,14 @@ defmodule Pleroma.Hashtag do
|
|||
|> String.trim()
|
||||
end
|
||||
|
||||
def get_by_id(id) do
|
||||
Repo.get(Hashtag, id)
|
||||
end
|
||||
|
||||
def get_by_name(name) do
|
||||
Repo.get_by(Hashtag, name: normalize_name(name))
|
||||
end
|
||||
|
||||
def get_or_create_by_name(name) do
|
||||
changeset = changeset(%Hashtag{}, %{name: name})
|
||||
|
||||
|
@ -103,4 +112,22 @@ defmodule Pleroma.Hashtag do
|
|||
{:ok, deleted_count}
|
||||
end
|
||||
end
|
||||
|
||||
def get_followers(%Hashtag{id: hashtag_id}) do
|
||||
from(hf in HashtagFollow)
|
||||
|> where([hf], hf.hashtag_id == ^hashtag_id)
|
||||
|> join(:inner, [hf], u in assoc(hf, :user))
|
||||
|> select([hf, u], u.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}})
|
||||
when is_list(tags) do
|
||||
tags
|
||||
|> Enum.map(&get_followers/1)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
def get_recipients_for_activity(_activity), do: []
|
||||
end
|
||||
|
|
|
@ -116,7 +116,11 @@ defmodule Pleroma.Object.Fetcher do
|
|||
|
||||
# Note: will create a Create activity, which we need internally at the moment.
|
||||
def fetch_object_from_id(id, options \\ []) do
|
||||
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
||||
with %URI{} = uri <- URI.parse(id),
|
||||
# If we have instance restrictions, apply them here to prevent fetching from unwanted instances
|
||||
{:ok, nil} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri),
|
||||
{:ok, _} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri),
|
||||
{_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
||||
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
|
||||
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
||||
{_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
|
||||
|
@ -155,6 +159,9 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:fetch, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:reject, reason} ->
|
||||
{:reject, reason}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
|
@ -180,7 +187,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:error, %Tesla.Mock.Error{}} ->
|
||||
nil
|
||||
|
||||
{:error, "Object has been deleted"} ->
|
||||
{:error, {"Object has been deleted", _id, _code}} ->
|
||||
nil
|
||||
|
||||
{:reject, reason} ->
|
||||
|
@ -284,7 +291,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
|
||||
{:ok, %{status: code}} when code in [404, 410] ->
|
||||
{:error, "Object has been deleted"}
|
||||
{:error, {"Object has been deleted", id, code}}
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
|
|
@ -30,7 +30,7 @@ defimpl Elasticsearch.Document, for: Pleroma.Activity do
|
|||
trimmed
|
||||
end
|
||||
|
||||
if String.length(content) > 1 do
|
||||
if String.length(content) > 1 and not is_nil(data["published"]) do
|
||||
{:ok, published, _} = DateTime.from_iso8601(data["published"])
|
||||
|
||||
%{
|
||||
|
|
|
@ -128,7 +128,7 @@ defmodule Pleroma.Search.Meilisearch do
|
|||
trimmed
|
||||
end
|
||||
|
||||
if String.length(content) > 1 do
|
||||
if String.length(content) > 1 and not is_nil(data["published"]) do
|
||||
{:ok, published, _} = DateTime.from_iso8601(data["published"])
|
||||
|
||||
%{
|
||||
|
|
|
@ -18,6 +18,8 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Emoji
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.User.HashtagFollow
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.MFA
|
||||
|
@ -168,6 +170,12 @@ defmodule Pleroma.User do
|
|||
|
||||
has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
|
||||
|
||||
many_to_many(:followed_hashtags, Hashtag,
|
||||
on_replace: :delete,
|
||||
on_delete: :delete_all,
|
||||
join_through: HashtagFollow
|
||||
)
|
||||
|
||||
for {relationship_type,
|
||||
[
|
||||
{outgoing_relation, outgoing_relation_target},
|
||||
|
@ -1914,7 +1922,7 @@ defmodule Pleroma.User do
|
|||
{:ok, user}
|
||||
|
||||
e ->
|
||||
Logger.error("Could not fetch user, #{inspect(e)}")
|
||||
Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
@ -2550,4 +2558,54 @@ defmodule Pleroma.User do
|
|||
_ -> {:error, user}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
|
||||
when is_list(follows),
|
||||
do: user
|
||||
|
||||
defp maybe_load_followed_hashtags(%User{} = user) do
|
||||
followed_hashtags = HashtagFollow.get_by_user(user)
|
||||
%{user | followed_hashtags: followed_hashtags}
|
||||
end
|
||||
|
||||
def followed_hashtags(%User{followed_hashtags: follows})
|
||||
when is_list(follows),
|
||||
do: follows
|
||||
|
||||
def followed_hashtags(%User{} = user) do
|
||||
{:ok, user} =
|
||||
user
|
||||
|> maybe_load_followed_hashtags()
|
||||
|> set_cache()
|
||||
|
||||
user.followed_hashtags
|
||||
end
|
||||
|
||||
def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
|
||||
Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
|
||||
user = maybe_load_followed_hashtags(user)
|
||||
|
||||
with {:ok, _} <- HashtagFollow.new(user, hashtag),
|
||||
follows <- HashtagFollow.get_by_user(user),
|
||||
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
|
||||
user
|
||||
|> set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
|
||||
Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
|
||||
user = maybe_load_followed_hashtags(user)
|
||||
|
||||
with {:ok, _} <- HashtagFollow.delete(user, hashtag),
|
||||
follows <- HashtagFollow.get_by_user(user),
|
||||
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
|
||||
user
|
||||
|> set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
|
||||
not is_nil(HashtagFollow.get(user, hashtag))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
defmodule Pleroma.User.HashtagFollow do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Repo
|
||||
|
||||
schema "user_follows_hashtag" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:hashtag, Hashtag)
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do
|
||||
user_hashtag_follow
|
||||
|> cast(attrs, [:user_id, :hashtag_id])
|
||||
|> unique_constraint(:hashtag_id,
|
||||
name: :user_hashtag_follows_user_id_hashtag_id_index,
|
||||
message: "already following"
|
||||
)
|
||||
|> validate_required([:user_id, :hashtag_id])
|
||||
end
|
||||
|
||||
def new(%User{} = user, %Hashtag{} = hashtag) do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{user_id: user.id, hashtag_id: hashtag.id})
|
||||
|> Repo.insert(on_conflict: :nothing)
|
||||
end
|
||||
|
||||
def delete(%User{} = user, %Hashtag{} = hashtag) do
|
||||
with %__MODULE__{} = user_hashtag_follow <- get(user, hashtag) do
|
||||
Repo.delete(user_hashtag_follow)
|
||||
else
|
||||
_ -> {:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def get(%User{} = user, %Hashtag{} = hashtag) do
|
||||
from(hf in __MODULE__)
|
||||
|> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def get_by_user(%User{} = user) do
|
||||
Ecto.assoc(user, :followed_hashtags)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
|
@ -62,6 +62,11 @@ defmodule Pleroma.User.Search do
|
|||
end
|
||||
end
|
||||
|
||||
def sanitise_domain(domain) do
|
||||
domain
|
||||
|> String.replace(~r/[!-\,|@|?|<|>|[-`|{-~|\/|:|\s]+/, "")
|
||||
end
|
||||
|
||||
defp format_query(query_string) do
|
||||
# Strip the beginning @ off if there is a query
|
||||
query_string = String.trim_leading(query_string, "@")
|
||||
|
@ -69,7 +74,7 @@ defmodule Pleroma.User.Search do
|
|||
with [name, domain] <- String.split(query_string, "@") do
|
||||
encoded_domain =
|
||||
domain
|
||||
|> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
|
||||
|> sanitise_domain()
|
||||
|> String.to_charlist()
|
||||
|> :idna.encode()
|
||||
|> to_string()
|
||||
|
|
|
@ -739,9 +739,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> fetch_activities(params, :offset)
|
||||
end
|
||||
|
||||
defp user_activities_recipients(%{godmode: true}), do: []
|
||||
def user_activities_recipients(%{godmode: true}), do: []
|
||||
|
||||
defp user_activities_recipients(%{reading_user: reading_user}) do
|
||||
def user_activities_recipients(%{reading_user: reading_user}) do
|
||||
if not is_nil(reading_user) and reading_user.local do
|
||||
[
|
||||
Constants.as_public(),
|
||||
|
@ -933,6 +933,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
)
|
||||
end
|
||||
|
||||
# Essentially, either look for activities addressed to `recipients`, _OR_ ones
|
||||
# that reference a hashtag that the user follows
|
||||
# Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
|
||||
# follow any
|
||||
defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
|
||||
restrict_recipients(query, recipients, user)
|
||||
end
|
||||
|
||||
defp restrict_recipients_or_hashtags(query, recipients, user, []) do
|
||||
restrict_recipients(query, recipients, user)
|
||||
end
|
||||
|
||||
defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
|
||||
from([activity, object] in query)
|
||||
|> join(:left, [activity, object], hto in "hashtags_objects",
|
||||
on: hto.object_id == object.id,
|
||||
as: :hto
|
||||
)
|
||||
|> where(
|
||||
[activity, object, hto: hto],
|
||||
(hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
|
||||
fragment("? && ?", ^recipients, activity.recipients)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_local(query, %{local_only: true}) do
|
||||
from(activity in query, where: activity.local == true)
|
||||
end
|
||||
|
@ -1380,7 +1405,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_preload_report_notes(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> maybe_order(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_since(opts)
|
||||
|> restrict_local(opts)
|
||||
|
@ -1686,7 +1711,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
{:ok, maybe_update_follow_information(data)}
|
||||
else
|
||||
# If this has been deleted, only log a debug and not an error
|
||||
{:error, "Object has been deleted" = e} ->
|
||||
{:error, {"Object has been deleted", _, _} = e} ->
|
||||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
|
||||
@required_description_keys [:key, :related_policy]
|
||||
|
||||
def filter_one(policy, %{"type" => type} = message)
|
||||
when type in ["Undo", "Block", "Delete"] and
|
||||
policy != Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
def filter_one(policy, message) do
|
||||
should_plug_history? =
|
||||
if function_exported?(policy, :history_awareness, 0) do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
|
||||
@moduledoc "Adds expiration to all local Create activities"
|
||||
@moduledoc "Adds expiration to all local Create/Update activities"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
|
@ -25,8 +25,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
|
|||
String.starts_with?(actor, Pleroma.Web.Endpoint.url())
|
||||
end
|
||||
|
||||
defp note?(activity) do
|
||||
match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity)
|
||||
defp note?(%{"type" => type, "object" => %{"type" => "Note"}})
|
||||
when type in ["Create", "Update"] do
|
||||
true
|
||||
end
|
||||
|
||||
defp note?(_) do
|
||||
false
|
||||
end
|
||||
|
||||
defp maybe_add_expiration(activity) do
|
||||
|
|
|
@ -29,7 +29,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
|
|||
defp contains_links?(_), do: false
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
|
||||
def filter(%{"type" => type, "actor" => actor, "object" => object} = message)
|
||||
when type in ["Create", "Update"] do
|
||||
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:contains_links, true} <- {:contains_links, contains_links?(object)},
|
||||
{:old_user, true} <- {:old_user, old_user?(u)} do
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def filter(message) do
|
||||
with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]),
|
||||
%User{actor_type: "Service"} = follower <-
|
||||
User.get_cached_by_nickname(follower_nickname),
|
||||
%{"type" => "Create", "object" => %{"type" => "Note"}} <- message do
|
||||
try_follow(follower, message)
|
||||
else
|
||||
nil ->
|
||||
Logger.warn(
|
||||
"#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname
|
||||
account does not exist, or the account is not correctly configured as a bot."
|
||||
)
|
||||
|
||||
{:ok, message}
|
||||
|
||||
_ ->
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp try_follow(follower, message) do
|
||||
to = Map.get(message, "to", [])
|
||||
cc = Map.get(message, "cc", [])
|
||||
actor = [message["actor"]]
|
||||
|
||||
Enum.concat([to, cc, actor])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> User.get_all_by_ap_id()
|
||||
|> Enum.each(fn user ->
|
||||
with false <- user.local,
|
||||
false <- User.following?(follower, user),
|
||||
false <- User.locked?(user),
|
||||
false <- (user.bio || "") |> String.downcase() |> String.contains?("nobot") do
|
||||
Logger.debug(
|
||||
"#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}"
|
||||
)
|
||||
|
||||
CommonAPI.follow(follower, user)
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
|
@ -17,13 +17,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
|
|||
@impl true
|
||||
def filter(
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
) do
|
||||
)
|
||||
when type in ["Create", "Update"] do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
isbot = check_if_bot(user)
|
||||
|
||||
|
|
|
@ -73,8 +73,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message)
|
||||
when object_type in ~w{Note Article} do
|
||||
def filter(%{"type" => type, "object" => %{"type" => object_type}} = message)
|
||||
when type in ~w{Create Update} and object_type in ~w{Note Article} do
|
||||
reject_threshold =
|
||||
Pleroma.Config.get(
|
||||
[:mrf_hellthread, :reject_threshold],
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
|
|||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create"} = message) do
|
||||
def filter(%{"type" => type} = message) when type in ["Create", "Update"] do
|
||||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||
|
||||
|
|
|
@ -13,20 +13,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
defp check_accept(%{host: actor_host} = _actor_info, object) do
|
||||
def check_accept(%{host: actor_host} = _actor_info) do
|
||||
accepts =
|
||||
instance_list(:accept)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
cond do
|
||||
accepts == [] -> {:ok, object}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
||||
accepts == [] -> {:ok, nil}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, nil}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, nil}
|
||||
true -> {:reject, "[SimplePolicy] host not in accept list"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reject(%{host: actor_host} = _actor_info, object) do
|
||||
def check_reject(%{host: actor_host} = _actor_info) do
|
||||
rejects =
|
||||
instance_list(:reject)
|
||||
|> MRF.subdomains_regex()
|
||||
|
@ -34,15 +34,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
if MRF.subdomain_match?(rejects, actor_host) do
|
||||
{:reject, "[SimplePolicy] host in reject list"}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_media_removal(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
|
||||
%{"type" => type, "object" => %{"attachment" => child_attachment}} = object
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
when type in ["Create", "Update"] and length(child_attachment) > 0 do
|
||||
media_removal =
|
||||
instance_list(:media_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
@ -63,10 +63,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
defp check_media_nsfw(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"object" => %{} = _child_object
|
||||
} = object
|
||||
) do
|
||||
)
|
||||
when type in ["Create", "Update"] do
|
||||
media_nsfw =
|
||||
instance_list(:media_nsfw)
|
||||
|> MRF.subdomains_regex()
|
||||
|
@ -177,6 +178,55 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_banner_removal(_actor_info, object), do: {:ok, object}
|
||||
|
||||
defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do
|
||||
rest
|
||||
|> String.split(",", parts: 2, trim: true)
|
||||
|> hd()
|
||||
|> case do
|
||||
nil -> nil
|
||||
hostname -> URI.parse("//" <> hostname)
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_context_uri(%{"context" => "http" <> _ = context}), do: URI.parse(context)
|
||||
|
||||
defp extract_context_uri(_), do: nil
|
||||
|
||||
defp check_context(activity) do
|
||||
uri = extract_context_uri(activity)
|
||||
|
||||
with {:uri, true} <- {:uri, Kernel.match?(%URI{}, uri)},
|
||||
{:ok, _} <- check_accept(uri),
|
||||
{:ok, _} <- check_reject(uri) do
|
||||
{:ok, activity}
|
||||
else
|
||||
# Can't check.
|
||||
{:uri, false} -> {:ok, activity}
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}} = activity) do
|
||||
with {:ok, _} <- filter(in_reply_to) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reply_to(activity), do: {:ok, activity}
|
||||
|
||||
defp maybe_check_thread(activity) do
|
||||
if Config.get([:mrf_simple, :handle_threads], true) do
|
||||
with {:ok, _} <- check_context(activity),
|
||||
{:ok, _} <- check_reply_to(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
else
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_object(%{"object" => object} = activity) do
|
||||
with {:ok, _object} <- filter(object) do
|
||||
{:ok, activity}
|
||||
|
@ -209,13 +259,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
def filter(%{"actor" => actor} = object) do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
with {:ok, object} <- check_accept(actor_info, object),
|
||||
{:ok, object} <- check_reject(actor_info, object),
|
||||
with {:ok, _} <- check_accept(actor_info),
|
||||
{:ok, _} <- check_reject(actor_info),
|
||||
{:ok, object} <- check_media_removal(actor_info, object),
|
||||
{:ok, object} <- check_media_nsfw(actor_info, object),
|
||||
{:ok, object} <- check_ftl_removal(actor_info, object),
|
||||
{:ok, object} <- check_followers_only(actor_info, object),
|
||||
{:ok, object} <- check_report_removal(actor_info, object),
|
||||
{:ok, object} <- maybe_check_thread(object),
|
||||
{:ok, object} <- check_object(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
|
@ -229,8 +280,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
with {:ok, object} <- check_accept(actor_info, object),
|
||||
{:ok, object} <- check_reject(actor_info, object),
|
||||
with {:ok, _} <- check_accept(actor_info),
|
||||
{:ok, _} <- check_reject(actor_info),
|
||||
{:ok, object} <- check_avatar_removal(actor_info, object),
|
||||
{:ok, object} <- check_banner_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
|
@ -241,11 +292,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
end
|
||||
end
|
||||
|
||||
def filter(%{"id" => id} = object) do
|
||||
with {:ok, _} <- filter(id) do
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(object) when is_binary(object) do
|
||||
uri = URI.parse(object)
|
||||
|
||||
with {:ok, object} <- check_accept(uri, object),
|
||||
{:ok, object} <- check_reject(uri, object) do
|
||||
with {:ok, _} <- check_accept(uri),
|
||||
{:ok, _} <- check_reject(uri) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
|
@ -287,6 +344,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
mrf_simple_excluded =
|
||||
Config.get(:mrf_simple)
|
||||
|> Enum.filter(fn {_, v} -> is_list(v) end)
|
||||
|> Enum.map(fn {rule, instances} ->
|
||||
{rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
|
||||
end)
|
||||
|
@ -331,66 +389,78 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
label: "MRF Simple",
|
||||
description: "Simple ingress policies",
|
||||
children:
|
||||
[
|
||||
%{
|
||||
key: :media_removal,
|
||||
description:
|
||||
"List of instances to strip media attachments from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :media_nsfw,
|
||||
label: "Media NSFW",
|
||||
description:
|
||||
"List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal,
|
||||
description:
|
||||
"List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :reject,
|
||||
description:
|
||||
"List of instances to reject activities from (except deletes) and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :accept,
|
||||
description:
|
||||
"List of instances to only accept activities from (except deletes) and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :followers_only,
|
||||
description:
|
||||
"Force posts from the given instances to be visible by followers only and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :report_removal,
|
||||
description: "List of instances to reject reports from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :avatar_removal,
|
||||
description: "List of instances to strip avatars from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :banner_removal,
|
||||
description: "List of instances to strip banners from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :reject_deletes,
|
||||
description: "List of instances to reject deletions from and the reason for doing so"
|
||||
}
|
||||
]
|
||||
|> Enum.map(fn setting ->
|
||||
Map.merge(
|
||||
setting,
|
||||
([
|
||||
%{
|
||||
key: :media_removal,
|
||||
description:
|
||||
"List of instances to strip media attachments from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :media_nsfw,
|
||||
label: "Media NSFW",
|
||||
description:
|
||||
"List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal,
|
||||
description:
|
||||
"List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :reject,
|
||||
description:
|
||||
"List of instances to reject activities from (except deletes) and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :accept,
|
||||
description:
|
||||
"List of instances to only accept activities from (except deletes) and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :followers_only,
|
||||
description:
|
||||
"Force posts from the given instances to be visible by followers only and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :report_removal,
|
||||
description: "List of instances to reject reports from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :avatar_removal,
|
||||
description: "List of instances to strip avatars from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :banner_removal,
|
||||
description: "List of instances to strip banners from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :reject_deletes,
|
||||
description: "List of instances to reject deletions from and the reason for doing so"
|
||||
}
|
||||
]
|
||||
|> Enum.map(fn setting ->
|
||||
Map.merge(
|
||||
setting,
|
||||
%{
|
||||
type: {:list, :tuple},
|
||||
key_placeholder: "instance",
|
||||
value_placeholder: "reason",
|
||||
suggestions: [
|
||||
{"example.com", "Some reason"},
|
||||
{"*.example.com", "Another reason"}
|
||||
]
|
||||
}
|
||||
)
|
||||
end)) ++
|
||||
[
|
||||
%{
|
||||
type: {:list, :tuple},
|
||||
key_placeholder: "instance",
|
||||
value_placeholder: "reason",
|
||||
suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]
|
||||
key: :handle_threads,
|
||||
label: "Apply to entire threads",
|
||||
type: :boolean,
|
||||
description:
|
||||
"Enable to filter replies to threads based from their originating instance, using the reject and accept rules"
|
||||
}
|
||||
)
|
||||
end)
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,22 +27,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
defp process_tag(
|
||||
"mrf_tag:media-force-nsfw",
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"object" => %{"attachment" => child_attachment}
|
||||
} = message
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
when length(child_attachment) > 0 and type in ["Create", "Update"] do
|
||||
{:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:media-strip",
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"object" => %{"attachment" => child_attachment} = object
|
||||
} = message
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
when length(child_attachment) > 0 and type in ["Create", "Update"] do
|
||||
object = Map.delete(object, "attachment")
|
||||
message = Map.put(message, "object", object)
|
||||
|
||||
|
@ -52,13 +52,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
defp process_tag(
|
||||
"mrf_tag:force-unlisted",
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
) do
|
||||
)
|
||||
when type in ["Create", "Update"] do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if Enum.member?(to, Pleroma.Constants.as_public()) do
|
||||
|
@ -85,13 +86,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
defp process_tag(
|
||||
"mrf_tag:sandbox",
|
||||
%{
|
||||
"type" => "Create",
|
||||
"type" => type,
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
) do
|
||||
)
|
||||
when type in ["Create", "Update"] do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if Enum.member?(to, Pleroma.Constants.as_public()) or
|
||||
|
@ -152,7 +154,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
do: filter_message(target_actor, message)
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor, "type" => "Create"} = message),
|
||||
def filter(%{"actor" => actor, "type" => type} = message) when type in ["Create", "Update"],
|
||||
do: filter_message(actor, message)
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -136,7 +136,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.drop(["conversation", "inReplyToAtomUri"])
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
Logger.warn("Couldn't fetch reply@#{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
else
|
||||
|
@ -159,7 +159,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("quoteUri", quoted_object.data["id"])
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
|
||||
Logger.warn("Couldn't fetch quote@#{inspect(quote_url)}, error: #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
else
|
||||
|
@ -833,7 +833,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
Map.put(data, "object", external_url)
|
||||
else
|
||||
{:fetch, e} ->
|
||||
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
|
||||
Logger.error("Couldn't fetch fixed_object@#{object} #{inspect(e)}")
|
||||
data
|
||||
|
||||
_ ->
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
defmodule Pleroma.Web.ApiSpec.TagOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Hashtag",
|
||||
description: "View a hashtag",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Follow a hashtag",
|
||||
description: "Follow a hashtag",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.follow",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def unfollow_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Unfollow a hashtag",
|
||||
description: "Unfollow a hashtag",
|
||||
security: [%{"oAuth" => ["write:follow"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.unfollow",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:path,
|
||||
%Schema{type: :string},
|
||||
"Name of the hashtag"
|
||||
)
|
||||
end
|
||||
end
|
|
@ -17,11 +17,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
|||
type: :string,
|
||||
format: :uri,
|
||||
description: "A link to the hashtag on the instance"
|
||||
},
|
||||
following: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether the authenticated user is following the hashtag"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
name: "cofe",
|
||||
url: "https://lain.com/tag/cofe"
|
||||
url: "https://lain.com/tag/cofe",
|
||||
following: false
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.TagController do
|
||||
@moduledoc "Hashtag routes for mastodon API"
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Hashtag
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:show])
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["write:follows"]} when action in [:follow, :unfollow]
|
||||
)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user)
|
||||
else
|
||||
_ -> conn |> render_error(:not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
|
||||
def follow(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||
%User{} = user <- conn.assigns.user,
|
||||
{:ok, _} <-
|
||||
User.follow_hashtag(user, hashtag) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||
else
|
||||
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||
%User{} = user <- conn.assigns.user,
|
||||
{:ok, _} <-
|
||||
User.unfollow_hashtag(user, hashtag) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||
else
|
||||
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,6 +41,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|
||||
# GET /api/v1/timelines/home
|
||||
def home(%{assigns: %{user: user}} = conn, params) do
|
||||
followed_hashtags =
|
||||
user
|
||||
|> User.followed_hashtags()
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|
@ -50,6 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|> Map.put(:announce_filtering_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:local_only, params[:local])
|
||||
|> Map.put(:followed_hashtags, followed_hashtags)
|
||||
|> Map.delete(:local)
|
||||
|
||||
activities =
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.TagView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
def render("show.json", %{tag: tag, for_user: user}) do
|
||||
following =
|
||||
with %User{} <- user do
|
||||
User.following_hashtag?(user, tag)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
|
||||
%{
|
||||
name: tag.name,
|
||||
url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name),
|
||||
history: [],
|
||||
following: following
|
||||
}
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.Plugs.StaticFEPlug do
|
|||
def init(options), do: options
|
||||
|
||||
def call(conn, _) do
|
||||
if enabled?() and requires_html?(conn) do
|
||||
if enabled?() and requires_html?(conn) and not_logged_in?(conn) do
|
||||
conn
|
||||
|> StaticFEController.call(:show)
|
||||
|> halt()
|
||||
|
@ -23,4 +23,7 @@ defmodule Pleroma.Web.Plugs.StaticFEPlug do
|
|||
defp requires_html?(conn) do
|
||||
Phoenix.Controller.get_format(conn) == "html"
|
||||
end
|
||||
|
||||
defp not_logged_in?(%{assigns: %{user: %Pleroma.User{}}}), do: false
|
||||
defp not_logged_in?(_), do: true
|
||||
end
|
||||
|
|
|
@ -150,6 +150,8 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
pipeline :static_fe do
|
||||
plug(:fetch_session)
|
||||
plug(:authenticate)
|
||||
plug(Pleroma.Web.Plugs.StaticFEPlug)
|
||||
end
|
||||
|
||||
|
@ -598,6 +600,10 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/announcements", AnnouncementController, :index)
|
||||
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
||||
|
||||
get("/tags/:id", TagController, :show)
|
||||
post("/tags/:id/follow", TagController, :follow)
|
||||
post("/tags/:id/unfollow", TagController, :unfollow)
|
||||
end
|
||||
|
||||
scope "/api/web", Pleroma.Web do
|
||||
|
@ -724,6 +730,12 @@ defmodule Pleroma.Web.Router do
|
|||
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.StaticFE do
|
||||
# Profile pages for static-fe
|
||||
get("/users/:nickname/with_replies", StaticFEController, :show)
|
||||
get("/users/:nickname/media", StaticFEController, :show)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
pipe_through(:accepts_html)
|
||||
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
|
||||
|
@ -767,10 +779,16 @@ defmodule Pleroma.Web.Router do
|
|||
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
|
||||
post("/api/ap/upload_media", ActivityPubController, :upload_media)
|
||||
|
||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
# Note: html format is supported only if static FE is enabled
|
||||
pipe_through([:accepts_html_json, :static_fe, :activitypub_client])
|
||||
|
||||
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
|
||||
get("/users/:nickname/followers", ActivityPubController, :followers)
|
||||
get("/users/:nickname/following", ActivityPubController, :following)
|
||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
|
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
|
||||
def show(%{assigns: %{username_or_id: username_or_id, tab: tab}} = conn, params) do
|
||||
with {_, %User{local: true} = user} <-
|
||||
{:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)},
|
||||
{_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
|
||||
|
@ -55,11 +55,36 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
params
|
||||
|> Map.take(@page_keys)
|
||||
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|> Map.put(:limit, 20)
|
||||
|
||||
params =
|
||||
case tab do
|
||||
"posts" ->
|
||||
Map.put(params, :exclude_replies, true)
|
||||
|
||||
"media" ->
|
||||
Map.put(params, :only_media, true)
|
||||
|
||||
_ ->
|
||||
params
|
||||
end
|
||||
|
||||
timeline =
|
||||
user
|
||||
|> ActivityPub.fetch_user_activities(_reading_user = nil, params)
|
||||
|> Enum.map(&represent/1)
|
||||
case tab do
|
||||
tab when tab in ["posts", "with_replies", "media"] ->
|
||||
user
|
||||
|> ActivityPub.fetch_user_activities(_reading_user = nil, params)
|
||||
|> Enum.map(&represent/1)
|
||||
|
||||
"following" when not user.hide_follows ->
|
||||
User.get_friends(user)
|
||||
|
||||
"followers" when not user.hide_followers ->
|
||||
User.get_followers(user)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
|
||||
prev_page_id =
|
||||
(params["min_id"] || params["max_id"]) &&
|
||||
|
@ -75,6 +100,11 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
meta: meta
|
||||
})
|
||||
else
|
||||
{_, %User{} = user} ->
|
||||
conn
|
||||
|> put_status(:found)
|
||||
|> redirect(external: user.uri || user.ap_id)
|
||||
|
||||
_ ->
|
||||
not_found(conn, "User not found.")
|
||||
end
|
||||
|
@ -150,6 +180,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
nil
|
||||
end
|
||||
|
||||
reply_to_user = in_reply_to_user(activity)
|
||||
|
||||
total_votes =
|
||||
if data["oneOf"] do
|
||||
Enum.sum(for option <- data["oneOf"], do: option["replies"]["totalItems"])
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
%{
|
||||
user: User.sanitize_html(user),
|
||||
title: get_title(activity.object),
|
||||
|
@ -160,10 +199,31 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
sensitive: data["sensitive"],
|
||||
selected: selected,
|
||||
counts: get_counts(activity),
|
||||
id: activity.id
|
||||
id: activity.id,
|
||||
visibility: Visibility.get_visibility(activity.object),
|
||||
reply_to: data["inReplyTo"],
|
||||
reply_to_user: reply_to_user,
|
||||
edited_at: data["updated"],
|
||||
poll: data["oneOf"],
|
||||
total_votes: total_votes
|
||||
}
|
||||
end
|
||||
|
||||
defp in_reply_to_user(%Activity{object: %Object{data: %{"inReplyTo" => inReplyTo}}} = activity)
|
||||
when is_binary(inReplyTo) do
|
||||
in_reply_to_activity = Activity.get_in_reply_to_activity(activity)
|
||||
|
||||
if in_reply_to_activity do
|
||||
in_reply_to_activity
|
||||
|> Map.get(:actor)
|
||||
|> User.get_cached_by_ap_id()
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp in_reply_to_user(_), do: nil
|
||||
|
||||
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
|
||||
do: assign(conn, :notice_id, notice_id)
|
||||
|
||||
|
@ -177,7 +237,16 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
do: assign(conn, :notice_id, notice_id)
|
||||
|
||||
defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
|
||||
do: assign(conn, :username_or_id, user_id)
|
||||
do:
|
||||
conn
|
||||
|> assign(:username_or_id, user_id)
|
||||
|> assign(:tab, "posts")
|
||||
|
||||
defp assign_id(%{path_info: ["users", user_id, tab]} = conn, _opts),
|
||||
do:
|
||||
conn
|
||||
|> assign(:username_or_id, user_id)
|
||||
|> assign(:tab, tab)
|
||||
|
||||
defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
|
||||
do: assign(conn, :object_id, object_id)
|
||||
|
|
|
@ -8,7 +8,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do
|
|||
alias Calendar.Strftime
|
||||
alias Pleroma.Emoji.Formatter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Gettext
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
|
@ -22,17 +21,38 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do
|
|||
Utils.fetch_media_type(@media_types, mediaType)
|
||||
end
|
||||
|
||||
def time_ago(date) do
|
||||
{:ok, date, _} = DateTime.from_iso8601(date)
|
||||
now = DateTime.utc_now()
|
||||
|
||||
Timex.from_now(date, now)
|
||||
end
|
||||
|
||||
def format_date(date) do
|
||||
{:ok, date, _} = DateTime.from_iso8601(date)
|
||||
Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
|
||||
end
|
||||
|
||||
def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
|
||||
def instance_name, do: Pleroma.Config.get([:instance, :name], "Akkoma")
|
||||
|
||||
def open_content? do
|
||||
Pleroma.Config.get(
|
||||
[:frontend_configurations, :collapse_message_with_subjects],
|
||||
true
|
||||
false
|
||||
)
|
||||
end
|
||||
|
||||
def get_attachment_name(%{"name" => name}), do: name
|
||||
|
||||
def get_attachment_name(_), do: ""
|
||||
|
||||
def poll_percentage(count, total_votes) do
|
||||
case count do
|
||||
0 ->
|
||||
"0%"
|
||||
|
||||
_ ->
|
||||
Integer.to_string(trunc(count / total_votes * 100)) <> "%"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Web.Streamer do
|
|||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.StreamerView
|
||||
require Pleroma.Constants
|
||||
|
||||
@mix_env Mix.env()
|
||||
@registry Pleroma.Web.StreamerRegistry
|
||||
|
@ -252,7 +253,17 @@ defmodule Pleroma.Web.Streamer do
|
|||
User.get_recipients_from_activity(item)
|
||||
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
|
||||
|
||||
Enum.each(recipient_topics, fn topic ->
|
||||
hashtag_recipients =
|
||||
if Pleroma.Constants.as_public() in item.recipients do
|
||||
Pleroma.Hashtag.get_recipients_for_activity(item)
|
||||
|> Enum.map(fn id -> "user:#{id}" end)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
all_recipients = Enum.uniq(recipient_topics ++ hashtag_recipients)
|
||||
|
||||
Enum.each(all_recipients, fn topic ->
|
||||
push_to_socket(topic, item)
|
||||
end)
|
||||
end
|
||||
|
|
|
@ -6,10 +6,39 @@
|
|||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
||||
<link rel="stylesheet" href="/static-fe/static-fe.css">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="background-image"></div>
|
||||
<nav>
|
||||
<div class="inner-nav">
|
||||
<a class="site-brand" href="/">
|
||||
<img class="favicon" src="/favicon.png" />
|
||||
<span><%= Pleroma.Config.get([:instance, :name]) %></span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<%= @inner_content %>
|
||||
<div class="underlay"></div>
|
||||
<div class="column main">
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
<div class="column sidebar">
|
||||
<div class="about panel">
|
||||
<div class="panel-heading">
|
||||
<%= gettext("About %{instance}", instance: Pleroma.Config.get([:instance, :name])) %>
|
||||
</div>
|
||||
<div class="about-content">
|
||||
<%= raw render_html("/static/terms-of-service.html") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--background-image: url("<%= Pleroma.Config.get([:instance, :background_image]) %>");
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
<%= case @mediaType do %>
|
||||
<% "audio" -> %>
|
||||
<audio class="u-audio" src="<%= @url %>" controls="controls"></audio>
|
||||
<% "video" -> %>
|
||||
<video class="u-video" src="<%= @url %>" controls="controls"></video>
|
||||
<% _ -> %>
|
||||
<img class="u-photo" src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
|
||||
<% end %>
|
||||
<a class="attachment" href="<%= @url %>" alt=<%= @name %>" title="<%= @name %>">
|
||||
<%= if @nsfw do %>
|
||||
<div class="nsfw-banner">
|
||||
<div><%= gettext("Hover to show content") %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= case @mediaType do %>
|
||||
<% "audio" -> %>
|
||||
<audio class="u-audio" src="<%= @url %>" controls="controls"></audio>
|
||||
<% "video" -> %>
|
||||
<video class="u-video" src="<%= @url %>" controls="controls"></video>
|
||||
<% _ -> %>
|
||||
<img class="u-photo" src="<%= @url %>">
|
||||
<% end %>
|
||||
</a>
|
||||
|
|
|
@ -1,41 +1,111 @@
|
|||
<div class="activity h-entry" <%= if @selected do %> id="selected" <% end %>>
|
||||
<p class="pull-right">
|
||||
<a class="activity-link u-url u-uid" href="<%= @link %>">
|
||||
<time class="dt-published" datetime="<%= @published %>">
|
||||
<%= format_date(@published) %>
|
||||
</time>
|
||||
<div class="status-container" <%= if @selected do %> id="selected" <% end %>>
|
||||
<div class="left-side">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" rel="author noopener">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="u-photo" width="48" height="48"
|
||||
src="<%= User.avatar_url(@user) |> MediaProxy.url %>"
|
||||
title="<%= @user.nickname %>" alt="<%= @user.nickname %>"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</p>
|
||||
<%= render("_user_card.html", %{user: @user}) %>
|
||||
<div class="activity-content">
|
||||
<%= if @title != "" do %>
|
||||
<details <%= if open_content?() do %>open<% end %>>
|
||||
<summary class="p-name"><%= raw @title %></summary>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
</details>
|
||||
<% else %>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
<% end %>
|
||||
<%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
|
||||
<%= if @sensitive do %>
|
||||
<details class="nsfw">
|
||||
<summary><%= Gettext.gettext("sensitive media") %></summary>
|
||||
<div>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
</div>
|
||||
</details>
|
||||
<% else %>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @selected do %>
|
||||
<dl class="counts">
|
||||
<dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
|
||||
<dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
|
||||
<dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
<div class="right-side">
|
||||
<div class="status-heading">
|
||||
<div class="heading-name-row">
|
||||
<div class="heading-left">
|
||||
<h4 class="username">
|
||||
<%= raw Formatter.emojify(@user.name, @user.emoji) %>
|
||||
</h4>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" class="account-name">
|
||||
<%= @user.nickname %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="heading-right">
|
||||
<a class="timeago" href="<%= @link %>">
|
||||
<time
|
||||
class="dt-published" datetime="<%= @published %>"
|
||||
title="<%= format_date(@published) %>"
|
||||
>
|
||||
<%= time_ago(@published) %>
|
||||
</time>
|
||||
</a>
|
||||
<%= if @visibility == "public" do %>
|
||||
<img class="fa-icon" src="/static-fe/svg/globe-solid.svg">
|
||||
<% else %>
|
||||
<%= if @visibility == "unlisted" do %>
|
||||
<img class="fa-icon" src="/static-fe/svg/lock-open-solid.svg">
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= if @reply_to do %>
|
||||
<div class="heading-reply-row">
|
||||
<a class="reply-to-link" href="<%= @reply_to %>">
|
||||
<img class="fa-icon" src="/static-fe/svg/reply-solid.svg">
|
||||
<%= gettext("Reply to") %>
|
||||
</a>
|
||||
<span class="h-card">
|
||||
<%= if @reply_to_user do %>
|
||||
<a href="<%= (@reply_to_user.uri || @reply_to_user.ap_id) %>" class="u-url mention">
|
||||
@<%= @reply_to_user.nickname %>
|
||||
</a>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if @edited_at do %>
|
||||
<div class="heading-edited-row">
|
||||
<%= gettext("Edited %{timeago}", timeago: time_ago(@edited_at)) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<%= if @title && @title != "" do %>
|
||||
<span class="status-summary"><%= raw @title %></span>
|
||||
<details <%= if open_content?() do %>open<% end %>>
|
||||
<summary><%= gettext("Show content") %></summary>
|
||||
<% end %>
|
||||
<div class="status-body">
|
||||
<%= raw @content %>
|
||||
<%= if @poll && length(@poll) > 0 do %>
|
||||
<div class="poll">
|
||||
<%= for %{"name" => option, "replies" => %{"totalItems" => count}} <- @poll do %>
|
||||
<div class="poll-option" title="<%= count %>/<%= @total_votes %>">
|
||||
<span class="percentage"><%= poll_percentage(count, @total_votes) %></span>
|
||||
<span><%= raw option %></span>
|
||||
<div class="fill" style="width: <%= poll_percentage(count, @total_votes) %>"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if length(@attachment) > 0 do %>
|
||||
<div class="attachments">
|
||||
<%= for attachment = %{"url" => [url | _]} <- @attachment do %>
|
||||
<%= render("_attachment.html", %{name: get_attachment_name(attachment),
|
||||
url: url["href"], mediaType: fetch_media_type(url), nsfw: @sensitive}) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @title && @title != "" do %>
|
||||
</details>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- <div class="emoji-reactions"></div> -->
|
||||
<div class="status-actions">
|
||||
<div>
|
||||
<img class="fa-icon" src="/static-fe/svg/reply-solid.svg">
|
||||
<span class="action-count"><%= @counts.replies %></span>
|
||||
</div>
|
||||
<div>
|
||||
<img class="fa-icon" src="/static-fe/svg/retweet-solid.svg">
|
||||
<span class="action-count"><%= @counts.announces %></span>
|
||||
</div>
|
||||
<div>
|
||||
<img class="fa-icon" src="/static-fe/svg/star-regular.svg">
|
||||
<span class="action-count"><%= @counts.likes %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
<div class="p-author h-card">
|
||||
<a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>">
|
||||
<div class="avatar">
|
||||
<img class="u-photo" src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
|
||||
<div class="user-card">
|
||||
<div class="left-side">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" rel="author noopener">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="u-photo" width="48" height="48"
|
||||
src="<%= User.avatar_url(@user) |> MediaProxy.url %>"
|
||||
title="<%= @user.nickname %>" alt="<%= @user.nickname %>"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="username">
|
||||
<%= raw Formatter.emojify(@user.name, @user.emoji) %>
|
||||
</div>
|
||||
<span class="display-name">
|
||||
<bdi class="p-name"><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>
|
||||
<span class="nickname"><%= @user.nickname %></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" class="account-name">
|
||||
@<%= @user.nickname %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="conversation">
|
||||
<%= for activity <- @activities do %>
|
||||
<%= render("_notice.html", activity) %>
|
||||
<% end %>
|
||||
<div class="panel conversation">
|
||||
<div class="panel-heading">
|
||||
<%= gettext("Conversation") %>
|
||||
</div>
|
||||
</main>
|
||||
<%= for activity <- @activities do %>
|
||||
<%= render("_notice.html", activity) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<header>
|
||||
<h1><%= gettext("Oops") %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<p><%= @message %></p>
|
||||
</main>
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<%= gettext("Error") %>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<%= @message %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,31 +1,148 @@
|
|||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
|
||||
<h3>
|
||||
<form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
|
||||
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
|
||||
</form>
|
||||
<%= raw Formatter.emojify(@user.name, @user.emoji) %> |
|
||||
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
|
||||
</h3>
|
||||
<p><%= raw @user.bio %></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="activity-stream">
|
||||
<%= for activity <- @timeline do %>
|
||||
<%= render("_notice.html", Map.put(activity, :selected, false)) %>
|
||||
<% end %>
|
||||
<p id="pagination">
|
||||
<%= if @prev_page_id do %>
|
||||
<%= link "«", to: "?min_id=" <> @prev_page_id %>
|
||||
<% end %>
|
||||
<%= if @prev_page_id && @next_page_id, do: " | " %>
|
||||
<%= if @next_page_id do %>
|
||||
<%= link "»", to: "?max_id=" <> @next_page_id %>
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="panel profile">
|
||||
<div class="user-header">
|
||||
<div class="user-banner"></div>
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" rel="author noopener">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="u-photo" width="48" height="48"
|
||||
src="<%= User.avatar_url(@user) |> MediaProxy.url %>"
|
||||
title="<%= @user.nickname %>" alt="<%= @user.nickname %>"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<div class="user-summary">
|
||||
<div class="top-line">
|
||||
<span class="username">
|
||||
<%= raw Formatter.emojify(@user.name, @user.emoji) %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="bottom-line">
|
||||
<%= link "@#{@user.nickname}", to: (@user.uri || @user.ap_id), class: "account-name" %>
|
||||
<%= if @user.is_admin && @user.show_role do %>
|
||||
<span class="user-role"><%= gettext("Admin") %></span>
|
||||
<% end %>
|
||||
<%= if @user.is_moderator && @user.show_role do %>
|
||||
<span class="user-role"><%= gettext("Moderator") %></span>
|
||||
<% end %>
|
||||
<%= if @user.actor_type == "Service" do %>
|
||||
<span class="user-role"><%= gettext("Bot") %></span>
|
||||
<% end %>
|
||||
<%= if @user.is_locked do %>
|
||||
<img class="fa-icon" src="/static-fe/svg/lock-solid.svg">
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="remote-follow">
|
||||
<form method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
|
||||
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button type="submit" class="button-default"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-counts">
|
||||
<div class="user-count">
|
||||
<h5><%= gettext("Posts") %></h5>
|
||||
<span><%= @user.note_count %></span>
|
||||
</div>
|
||||
<div class="user-count">
|
||||
<h5><%= gettext("Following") %></h5>
|
||||
<span><%= if @user.hide_follows_count do gettext("Hidden") else @user.following_count end %></span>
|
||||
</div>
|
||||
<div class="user-count">
|
||||
<h5><%= gettext("Followers") %></h5>
|
||||
<span><%= if @user.hide_followers_count do gettext("Hidden") else @user.follower_count end %></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="user-bio"><%= raw Formatter.emojify(@user.bio, @user.emoji) %></span>
|
||||
</div>
|
||||
</main>
|
||||
<div class="user-profile-fields">
|
||||
<%= for field <- @user.fields do %>
|
||||
<div class="user-profile-field">
|
||||
<dt title="<%= field["name"] %>"><%= raw Formatter.emojify(field["name"], @user.emoji) %></dt>
|
||||
<dd title="<%= field["value"] %>"><%= raw Formatter.emojify(field["value"], @user.emoji) %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="tab-switcher">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>">
|
||||
<button class="button-default tab <%= if @tab == "posts" do %>active<% end %>">
|
||||
<%= gettext("Posts") %>
|
||||
</button>
|
||||
</a>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>/with_replies">
|
||||
<button class="button-default tab <%= if @tab == "with_replies" do %>active<% end %>">
|
||||
<%= gettext("With Replies") %>
|
||||
</button>
|
||||
</a>
|
||||
<%= unless @user.hide_follows do %>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>/following">
|
||||
<button class="button-default tab <%= if @tab == "following" do %>active<% end %>">
|
||||
<%= gettext("Following") %>
|
||||
</button>
|
||||
</a>
|
||||
<% end %>
|
||||
<%= unless @user.hide_followers do %>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>/followers">
|
||||
<button class="button-default tab <%= if @tab == "followers" do %>active<% end %>">
|
||||
<%= gettext("Followers") %>
|
||||
</button>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>/media">
|
||||
<button class="button-default tab <%= if @tab == "media" do %>active<% end %>">
|
||||
<%= gettext("Media") %>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<%= if @prev_page_id do %>
|
||||
<%= link gettext("Show newer"), to: "?min_id=" <> @prev_page_id, class: "load-posts" %>
|
||||
<% end %>
|
||||
<div class="activity-stream">
|
||||
<%= if @tab in ["posts", "with_replies", "media"] do %>
|
||||
<%= for activity <- @timeline do %>
|
||||
<%= if(activity.user.id != @user.id) do %>
|
||||
<div class="repeat-header">
|
||||
<div class="left-side">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" rel="author noopener">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="u-photo" width="48" height="48"
|
||||
src="<%= User.avatar_url(@user) |> MediaProxy.url %>"
|
||||
title="<%= @user.nickname %>" alt="<%= @user.nickname %>"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<span class="username">
|
||||
<a href="<%= (@user.uri || @user.ap_id) %>" class="account-name">
|
||||
<%= raw Formatter.emojify(@user.name, @user.emoji) %>
|
||||
</a>
|
||||
</span>
|
||||
<img class="fa-icon" src="/static-fe/svg/retweet-solid.svg">
|
||||
<%= gettext("repeated") %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render("_notice.html", Map.put(activity, :selected, false)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= for user <- @timeline do %>
|
||||
<%= render("_user_card.html", %{user: user}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @next_page_id do %>
|
||||
<%= link gettext("Show older"), to: "?max_id=" <> @next_page_id, class: "load-posts" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--user-banner: url("<%= Pleroma.User.banner_url(@user) %>");
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,4 +4,11 @@
|
|||
|
||||
defmodule Pleroma.Web.LayoutView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
|
||||
def render_html(file) do
|
||||
case :httpc.request(Pleroma.Web.Endpoint.url() <> file) do
|
||||
{:ok, {{_, 200, _}, _headers, body}} -> body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,11 @@ defmodule Pleroma.Web.ManifestView do
|
|||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable"
|
||||
},
|
||||
%{
|
||||
src: "/static/logo-512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png"
|
||||
}
|
||||
],
|
||||
theme_color: Config.get([:manifest, :theme_color]),
|
||||
|
|
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
|||
def project do
|
||||
[
|
||||
app: :pleroma,
|
||||
version: version("3.4.0"),
|
||||
version: version("3.5.0"),
|
||||
elixir: "~> 1.12",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
|
|
|
@ -5,8 +5,8 @@ msgstr ""
|
|||
"POT-Creation-Date: 2022-07-28 09:35+0000\n"
|
||||
"PO-Revision-Date: 2022-08-08 15:48+0000\n"
|
||||
"Last-Translator: sola <spla@mastodont.cat>\n"
|
||||
"Language-Team: Catalan <http://translate.akkoma.dev/projects/akkoma/"
|
||||
"akkoma-backend-config-descriptions/ca/>\n"
|
||||
"Language-Team: Catalan <http://translate.akkoma.dev/projects/akkoma/akkoma-"
|
||||
"backend-config-descriptions/ca/>\n"
|
||||
"Language: ca\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
|
|
@ -0,0 +1,526 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-12-07 13:44+0000\n"
|
||||
"PO-Revision-Date: 2022-12-07 15:39+0000\n"
|
||||
"Last-Translator: t1 <taaa@fedora.email>\n"
|
||||
"Language-Team: Indonesian <http://translate.akkoma.dev/projects/akkoma/"
|
||||
"akkoma-backend-static-pages/id/>\n"
|
||||
"Language: id\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.14\n"
|
||||
|
||||
## This file is a PO Template file.
|
||||
##
|
||||
## "msgid"s here are often extracted from source code.
|
||||
## Add new translations manually only if they're dynamic
|
||||
## translations that can't be statically extracted.
|
||||
##
|
||||
## Run "mix gettext.extract" to bring this file up to
|
||||
## date. Leave "msgstr"s empty as changing them here as no
|
||||
## effect: edit them in PO (.po) files instead.
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow authorization button"
|
||||
msgid "Authorize"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow error"
|
||||
msgid "Error fetching user"
|
||||
msgstr "Gagal memuat pengguna"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow header"
|
||||
msgid "Remote follow"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "placeholder text for auth code entry"
|
||||
msgid "Authentication code"
|
||||
msgstr "Kode autentikasi"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "placeholder text for password entry"
|
||||
msgid "Password"
|
||||
msgstr "Kata sandi"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "placeholder text for username entry"
|
||||
msgid "Username"
|
||||
msgstr "Nama pengguna"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow authorization button for login"
|
||||
msgid "Authorize"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow authorization button for mfa"
|
||||
msgid "Authorize"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow error"
|
||||
msgid "Error following account"
|
||||
msgstr "Gagal mengikuti akun"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow header, need login"
|
||||
msgid "Log in to follow"
|
||||
msgstr "Masuk untuk mengikuti"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow mfa header"
|
||||
msgid "Two-factor authentication"
|
||||
msgstr "Autentikasi dua faktor"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow success"
|
||||
msgid "Account followed!"
|
||||
msgstr "Akun diikuti!"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "placeholder text for account id"
|
||||
msgid "Your account ID, e.g. lain@quitter.se"
|
||||
msgstr "ID akunmu, cth. lain@quitter.se"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow authorization button for following with a remote account"
|
||||
msgid "Follow"
|
||||
msgstr "Ikuti"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow error"
|
||||
msgid "Error: %{error}"
|
||||
msgstr "Kesalahan: %{error}"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "remote follow header"
|
||||
msgid "Remotely follow %{nickname}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset button"
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset failed homepage link"
|
||||
msgid "Homepage"
|
||||
msgstr "Beranda"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset failed message"
|
||||
msgid "Password reset failed"
|
||||
msgstr "Gagal mengatur ulang kata sandi"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset form confirm password prompt"
|
||||
msgid "Confirmation"
|
||||
msgstr "Konfirmasi"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset form password prompt"
|
||||
msgid "Password"
|
||||
msgstr "Kata sandi"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset invalid token message"
|
||||
msgid "Invalid Token"
|
||||
msgstr "Token Tidak Sah"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset successful homepage link"
|
||||
msgid "Homepage"
|
||||
msgstr "Beranda"
|
||||
|
||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset successful message"
|
||||
msgid "Password changed!"
|
||||
msgstr "Kata sandi diubah!"
|
||||
|
||||
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
|
||||
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "tag feed description"
|
||||
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth authorization exists page title"
|
||||
msgid "Authorization exists"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth authorize approve button"
|
||||
msgid "Approve"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth authorize cancel button"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth authorize message"
|
||||
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth authorized page title"
|
||||
msgid "Successfully authorized"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth external provider page title"
|
||||
msgid "Sign in with external provider"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth external provider sign in button"
|
||||
msgid "Sign in with %{strategy}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth login button"
|
||||
msgid "Log In"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth login password prompt"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth login username prompt"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register nickname prompt"
|
||||
msgid "Pleroma Handle"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register nickname unchangeable warning"
|
||||
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page email prompt"
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page fill form prompt"
|
||||
msgid "If you'd like to register a new account, please provide the details below."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page login button"
|
||||
msgid "Proceed as existing user"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page login password prompt"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page login prompt"
|
||||
msgid "Alternatively, sign in to connect to existing account."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page login username prompt"
|
||||
msgid "Name or email"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page nickname prompt"
|
||||
msgid "Nickname"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page register button"
|
||||
msgid "Proceed as new user"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page title"
|
||||
msgid "Registration Details"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth register page title"
|
||||
msgid "This is the first time you visit! Please enter your Pleroma handle."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth scopes message"
|
||||
msgid "The following permissions will be granted"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
|
||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "oauth token code message"
|
||||
msgid "Token code is <br>%{token}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa auth code prompt"
|
||||
msgid "Authentication code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa auth page title"
|
||||
msgid "Two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa auth page use recovery code link"
|
||||
msgid "Enter a two-factor recovery code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa auth verify code button"
|
||||
msgid "Verify"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa recover page title"
|
||||
msgid "Two-factor recovery"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa recover recovery code prompt"
|
||||
msgid "Recovery code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa recover use 2fa code link"
|
||||
msgid "Enter a two-factor code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mfa recover verify recovery code button"
|
||||
msgid "Verify"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "static fe profile page remote follow button"
|
||||
msgid "Remote follow"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:163
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email header line"
|
||||
msgid "Hey %{nickname}, here is what you've missed!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:544
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email receiver address"
|
||||
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:538
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email sending reason"
|
||||
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email unsubscribe action"
|
||||
msgid "To unsubscribe, please go %{here}."
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email unsubscribe action link text"
|
||||
msgid "here"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mailer unsubscribe failed message"
|
||||
msgid "UNSUBSCRIBE FAILURE"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "mailer unsubscribe successful message"
|
||||
msgid "UNSUBSCRIBE SUCCESSFUL"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/web/templates/email/digest.html.eex:385
|
||||
#, elixir-format
|
||||
msgctxt "new followers count header"
|
||||
msgid "%{count} New Follower"
|
||||
msgid_plural "%{count} New Followers"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:356
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "account archive email body - self-requested"
|
||||
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:384
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "account archive email subject"
|
||||
msgid "Your account archive is ready"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:188
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "approval pending email body"
|
||||
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:202
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "approval pending email subject"
|
||||
msgid "Your account is awaiting approval"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:158
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "confirmation email body"
|
||||
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:174
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "confirmation email subject"
|
||||
msgid "%{instance_name} account confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:310
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "digest email subject"
|
||||
msgid "Your digest from %{instance_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:81
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset email body"
|
||||
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:98
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "password reset email subject"
|
||||
msgid "Password reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:215
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "successful registration email body"
|
||||
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:231
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "successful registration email subject"
|
||||
msgid "Account registered on %{instance_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:119
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "user invitation email body"
|
||||
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:136
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "user invitation email subject"
|
||||
msgid "Invitation to %{instance_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "welcome email html body"
|
||||
msgid "Welcome to %{instance_name}!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "welcome email subject"
|
||||
msgid "Welcome to %{instance_name}!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "welcome email text body"
|
||||
msgid "Welcome to %{instance_name}!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/pleroma/emails/user_email.ex:368
|
||||
#, elixir-autogen, elixir-format
|
||||
msgctxt "account archive email body - admin requested"
|
||||
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||
msgstr ""
|
|
@ -0,0 +1,12 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:user_follows_hashtag) do
|
||||
add(:hashtag_id, references(:hashtags))
|
||||
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
end
|
||||
|
||||
create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id]))
|
||||
end
|
||||
end
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
@ -1,59 +1,278 @@
|
|||
/* pleroma-light and pleroma-dark themes from pleroma-fe */
|
||||
:root {
|
||||
--icon-filter: invert(38%) sepia(11%) saturate(209%) hue-rotate(179deg) brightness(99%) contrast(89%);
|
||||
--wallpaper: rgba(11, 16, 23, 1);
|
||||
--alertNeutral: rgba(185, 185, 186, 0.5);
|
||||
--alertNeutralText: rgba(255, 255, 255, 1);
|
||||
--avatarShadow: 0px 1px 8px 0px rgba(0, 0, 0, 0.7);
|
||||
--loadPostsSelected: rgba(23, 34, 46, 1);
|
||||
--loadPostsSelectedText: rgba(185, 185, 186, 1);
|
||||
--profileBg: rgba(7, 12, 17, 1);
|
||||
--profileTint: rgba(15, 22, 30, 0.5);
|
||||
--btnText: rgba(185, 185, 186, 1);
|
||||
--btn: rgba(21, 30, 43, 1);
|
||||
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
--btnHoverShadow: 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
--lightText: rgba(236, 236, 236, 1);
|
||||
--panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5) , 0px 4px 6px 3px rgba(0, 0, 0, 0.3);
|
||||
--panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
--topBar: rgba(21, 30, 43, 1);
|
||||
--topBarText: rgba(159, 159, 161, 1);
|
||||
--topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4) , 0px 2px 7px 0px rgba(0, 0, 0, 0.3);
|
||||
--underlay: rgba(9, 14, 20, 0.6);
|
||||
--background: rgba(15, 22, 30, 1);
|
||||
--faint: rgba(185, 185, 186, 0.5);
|
||||
--selectedPost: rgba(23, 34, 46, 1);
|
||||
--link: rgba(226, 177, 136, 1);
|
||||
--text: rgba(185, 185, 186, 1);
|
||||
--border: rgba(26, 37, 53, 1);
|
||||
--poll: rgba(99, 84, 72, 1);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);;
|
||||
--wallpaper: rgba(248, 250, 252, 1);
|
||||
--alertNeutral: rgba(48, 64, 85, 0.5);
|
||||
--alertNeutralText: rgba(0, 0, 0, 1);
|
||||
--avatarShadow: 0px 1px 8px 0px rgba(0, 0, 0, 0.7);
|
||||
--loadPostsSelected: rgba(224, 233, 240, 1);
|
||||
--loadPostsSelectedText: rgba(48, 64, 85, 1);
|
||||
--profileBg: rgba(128, 137, 146, 1);
|
||||
--profileTint: rgba(242, 246, 249, 0.5);
|
||||
--btnText: rgba(48, 64, 85, 1);
|
||||
--btn: rgba(214, 223, 237, 1);
|
||||
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
--btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
--lightText: rgba(11, 14, 19, 1);
|
||||
--panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5) , 0px 3px 6px 1px rgba(0, 0, 0, 0.2);
|
||||
--panelHeaderShadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
|
||||
--topBar: rgba(214, 223, 237, 1);
|
||||
--topBarText: rgba(48, 64, 85, 1);
|
||||
--topBarShadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.6);
|
||||
--underlay: rgba(93, 96, 134, 0.4);
|
||||
--background: rgba(242, 246, 249, 1);
|
||||
--faint: rgba(48, 64, 85, 0.5);
|
||||
--selectedPost: rgba(224, 233, 240, 1);
|
||||
--link: rgba(245, 91, 27, 1);
|
||||
--text: rgba(48, 64, 85, 1);
|
||||
--border: rgba(216, 230, 249, 1);
|
||||
--poll: rgba(243, 184, 160, 1);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #282c37;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 50px auto;
|
||||
max-width: 960px;
|
||||
padding: 40px;
|
||||
background-color: #313543;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 50px auto;
|
||||
max-width: 960px;
|
||||
padding: 40px;
|
||||
background-color: #313543;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.activity {
|
||||
border-radius: 4px;
|
||||
padding: 1em;
|
||||
padding-bottom: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
float: left;
|
||||
border-radius: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.activity-content img, video, audio {
|
||||
padding: 1em;
|
||||
max-width: 800px;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
#selected {
|
||||
background-color: #1b2735;
|
||||
}
|
||||
|
||||
.counts dt, .counts dd {
|
||||
float: left;
|
||||
margin-left: 1em;
|
||||
.background-image {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 3.5em;
|
||||
z-index: -1000;
|
||||
left: 0;
|
||||
right: -20px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--wallpaper);
|
||||
background-image: var(--background-image);
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 3.5em;
|
||||
background-color: var(--topBar);
|
||||
box-shadow: var(--topBarShadow);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.inner-nav {
|
||||
padding: 0 1.2em;
|
||||
margin: auto;
|
||||
max-width: 1110px;
|
||||
}
|
||||
|
||||
.inner-nav a {
|
||||
line-height: 3.5em;
|
||||
color: var(--topBarText);
|
||||
}
|
||||
|
||||
.inner-nav img {
|
||||
height: 28px;
|
||||
vertical-align: middle;
|
||||
padding-right: 5px
|
||||
}
|
||||
|
||||
body > .container {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(25em, 45em) 25em;
|
||||
grid-template-areas: "content sidebar";
|
||||
height: calc(100vh - 3.5em);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: span 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 1;
|
||||
background-color: var(--underlay);
|
||||
z-index: -1000;
|
||||
}
|
||||
|
||||
.column {
|
||||
padding: 1em;
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: var(--background);
|
||||
border-radius: 3px;
|
||||
box-shadow: var(--panelShadow);
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
background-color: var(--topBar);
|
||||
font-size: 1.3em;
|
||||
padding: 0.6em;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: var(--panelHeaderShadow);
|
||||
}
|
||||
|
||||
.about-content {
|
||||
padding: 0.6em;
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-area: content;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.status-container,
|
||||
.repeat-header,
|
||||
.user-card {
|
||||
display: flex;
|
||||
padding: 0.75em;
|
||||
}
|
||||
|
||||
.left-side {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.repeat-header {
|
||||
padding: 0.4em 0.75em;
|
||||
margin-bottom: -0.75em;
|
||||
}
|
||||
|
||||
.repeat-header .right-side {
|
||||
color: var(--faint);
|
||||
}
|
||||
.repeat-header .u-photo {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
.status-heading {
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-heading a {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.heading-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heading-name-row .account-name {
|
||||
min-width: 1.6em;
|
||||
margin-right: 0.4em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.heading-name-row .username,
|
||||
.repeat-header .username {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 85%;
|
||||
font-weight: bold;
|
||||
flex-shrink: 1;
|
||||
margin: 0;
|
||||
margin-right: 0.4em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.heading-name-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.heading-edited-row,
|
||||
.heading-reply-row {
|
||||
font-size: 0.85em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
.reply-to-link {
|
||||
color: var(--faint);
|
||||
}
|
||||
.reply-to-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#selected {
|
||||
background-color: var(--selectedPost);
|
||||
}
|
||||
|
||||
.timeago {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
#selected .timeago {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.timeago :hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.h-card {
|
||||
|
@ -69,116 +288,340 @@ header a:hover, .h-card a:hover {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
padding-top: 4px;
|
||||
.attachments {
|
||||
margin-top: 0.5em;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-content: stretch;
|
||||
max-height: 24em;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attachment > * {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.attachment:not(:last-child) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.nsfw-banner {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.nsfw-banner div {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nsfw-banner:not(:hover) {
|
||||
background-color: var(--background);
|
||||
}
|
||||
.nsfw-banner:hover div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.poll-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 0.75em 0.5em;
|
||||
padding: 0.1em 0.25em;
|
||||
word-break: break-word;
|
||||
z-index: 1;
|
||||
}
|
||||
.poll-option .percentage {
|
||||
width: 3.5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.poll-option .fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: var(--poll);
|
||||
border-radius: 3px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
.status-actions > * {
|
||||
max-width: 4em;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.status-summary {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
border-color: var(--border, #222);
|
||||
}
|
||||
|
||||
summary {
|
||||
text-align: center;
|
||||
color: var(--link);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-body {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 0.5em 26px;
|
||||
}
|
||||
|
||||
.user-info .container {
|
||||
padding: 18px 0 6px 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
max-height: 56px;
|
||||
}
|
||||
|
||||
.user-info a {
|
||||
color: var(--lightText);
|
||||
}
|
||||
|
||||
.user-info .avatar img {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-radius: 3px;
|
||||
box-shadow: var(--avatarShadow);
|
||||
}
|
||||
|
||||
.user-summary {
|
||||
display: block;
|
||||
margin-left: 0.6em;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 0;
|
||||
z-index: 1;
|
||||
line-height: 2em;
|
||||
color: var(--lightText);
|
||||
}
|
||||
|
||||
/* keep emoji from being hilariously huge */
|
||||
.display-name img {
|
||||
max-height: 1em;
|
||||
max-width: 1em;
|
||||
.button-default {
|
||||
user-select: none;
|
||||
color: var(--btnText);
|
||||
background-color: var(--btn);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--btnShadow);
|
||||
font-size: 1em;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.display-name .nickname {
|
||||
padding-top: 4px;
|
||||
.button-default:hover {
|
||||
box-shadow: var(--btnHoverShadow);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-bio {
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nickname:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.3;
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #9baec8;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
margin-bottom: 40px;
|
||||
.user-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: linear-gradient(to bottom, var(--profileTint), var(--profileTint)),
|
||||
var(--user-banner);
|
||||
background-size: cover;
|
||||
background-color: var(--profileBg);
|
||||
-webkit-mask: linear-gradient(to top, white, transparent) bottom no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
-webkit-mask-composite: xor;
|
||||
-webkit-mask-size: 100% 60%;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
.user-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
.user-role {
|
||||
color: var(--alertNeutralText);
|
||||
background-color: var(--alertNeutral);
|
||||
margin: 0 0.35em;
|
||||
padding: 0 0.25em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.user-profile-fields {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.user-profile-field {
|
||||
display: flex;
|
||||
margin: 0.25em;
|
||||
border: 1px solid var(--border, #222);
|
||||
border-radius: 3px;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-profile-field dt {
|
||||
padding: 0.5em 1.5em;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
background-color: rgba(0,0,0,.1);
|
||||
color: white;
|
||||
border: 0;
|
||||
border-bottom: 2px solid #9baec8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-bottom: 2px solid #4b8ed8;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: white;
|
||||
background-color: #419bdd;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 30px;
|
||||
text-transform: uppercase;
|
||||
flex: 0 1 30%;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: var(--lightText);
|
||||
border-right: 1px solid var(--border);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
.user-profile-field dd {
|
||||
padding: 0.5em 1.5em;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: #D8000C;
|
||||
background-color: #FFD2D2;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
flex: 1 1 30%;
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
.user-counts {
|
||||
display: flex;
|
||||
line-height: 1em;
|
||||
padding: 0.5em 1.5em 0 1.5em;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
color: var(--lightText);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-count {
|
||||
flex: 1 0 auto;
|
||||
padding: 0.5em 0;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.user-count h5 {
|
||||
font-size: 1em;
|
||||
font-weight: bolder;
|
||||
margin: 0 0 0.25em;
|
||||
}
|
||||
|
||||
.tab-switcher {
|
||||
display: flex;
|
||||
padding-top: 5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab-switcher::before,
|
||||
.tab-switcher::after {
|
||||
flex: 1 1 auto;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 1em;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.profile .status-container {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.load-posts {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: 3.5em;
|
||||
line-height: 3.5em;
|
||||
padding: 0 1em;
|
||||
width: 100%;
|
||||
color: #00529B;
|
||||
background-color: #BDE5F8;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
.load-posts:hover {
|
||||
background-color: var(--loadPostsSelected);
|
||||
color: var(--loadPostsSelectedText);
|
||||
}
|
||||
|
||||
|
||||
.fa-icon {
|
||||
height: 0.875em;
|
||||
margin: 0 0.3em;
|
||||
filter: var(--icon-filter);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.status-actions .fa-icon {
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
.reply-to-link .fa-icon {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
body > .container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.column {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img:not(.u-photo, .fa-icon) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.username img:not(.u-photo) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 21 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M352 144c0-44.2 35.8-80 80-80s80 35.8 80 80v48c0 17.7 14.3 32 32 32s32-14.3 32-32V144C576 64.5 511.5 0 432 0S288 64.5 288 144v48H64c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V256c0-35.3-28.7-64-64-64H352V144z"/></svg>
|
After Width: | Height: | Size: 485 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M144 144v48H304V144c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192V144C80 64.5 144.5 0 224 0s144 64.5 144 144v48h16c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256c0-35.3 28.7-64 64-64H80z"/></svg>
|
After Width: | Height: | Size: 460 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M205 34.8c11.5 5.1 19 16.6 19 29.2v64H336c97.2 0 176 78.8 176 176c0 113.3-81.5 163.9-100.2 174.1c-2.5 1.4-5.3 1.9-8.1 1.9c-10.9 0-19.7-8.9-19.7-19.7c0-7.5 4.3-14.4 9.8-19.5c9.4-8.8 22.2-26.4 22.2-56.7c0-53-43-96-96-96H224v64c0 12.6-7.4 24.1-19 29.2s-25 3-34.4-5.4l-160-144C3.9 225.7 0 217.1 0 208s3.9-17.7 10.6-23.8l160-144c9.4-8.5 22.9-10.6 34.4-5.4z"/></svg>
|
After Width: | Height: | Size: 599 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M272 416c17.7 0 32-14.3 32-32s-14.3-32-32-32H160c-17.7 0-32-14.3-32-32V192h32c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 128c0 53 43 96 96 96H272zM304 96c-17.7 0-32 14.3-32 32s14.3 32 32 32l112 0c17.7 0 32 14.3 32 32l0 128H416c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0V192c0-53-43-96-96-96L304 96z"/></svg>
|
After Width: | Height: | Size: 740 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M287.9 0C297.1 0 305.5 5.25 309.5 13.52L378.1 154.8L531.4 177.5C540.4 178.8 547.8 185.1 550.7 193.7C553.5 202.4 551.2 211.9 544.8 218.2L433.6 328.4L459.9 483.9C461.4 492.9 457.7 502.1 450.2 507.4C442.8 512.7 432.1 513.4 424.9 509.1L287.9 435.9L150.1 509.1C142.9 513.4 133.1 512.7 125.6 507.4C118.2 502.1 114.5 492.9 115.1 483.9L142.2 328.4L31.11 218.2C24.65 211.9 22.36 202.4 25.2 193.7C28.03 185.1 35.5 178.8 44.49 177.5L197.7 154.8L266.3 13.52C270.4 5.249 278.7 0 287.9 0L287.9 0zM287.9 78.95L235.4 187.2C231.9 194.3 225.1 199.3 217.3 200.5L98.98 217.9L184.9 303C190.4 308.5 192.9 316.4 191.6 324.1L171.4 443.7L276.6 387.5C283.7 383.7 292.2 383.7 299.2 387.5L404.4 443.7L384.2 324.1C382.9 316.4 385.5 308.5 391 303L476.9 217.9L358.6 200.5C350.7 199.3 343.9 194.3 340.5 187.2L287.9 78.95z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -161,6 +161,28 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
)
|
||||
end
|
||||
|
||||
test "does not fetch anything from a rejected instance" do
|
||||
clear_config([:mrf_simple, :reject], [{"evil.example.org", "i said so"}])
|
||||
|
||||
assert {:reject, _} =
|
||||
Fetcher.fetch_object_from_id("http://evil.example.org/@admin/99541947525187367")
|
||||
end
|
||||
|
||||
test "does not fetch anything if mrf_simple accept is on" do
|
||||
clear_config([:mrf_simple, :accept], [{"mastodon.example.org", "i said so"}])
|
||||
clear_config([:mrf_simple, :reject], [])
|
||||
|
||||
assert {:reject, _} =
|
||||
Fetcher.fetch_object_from_id(
|
||||
"http://notlisted.example.org/@admin/99541947525187367"
|
||||
)
|
||||
|
||||
assert {:ok, _object} =
|
||||
Fetcher.fetch_object_from_id(
|
||||
"http://mastodon.example.org/@admin/99541947525187367"
|
||||
)
|
||||
end
|
||||
|
||||
test "it resets instance reachability on successful fetch" do
|
||||
id = "http://mastodon.example.org/@admin/99541947525187367"
|
||||
Instances.set_consistently_unreachable(id)
|
||||
|
@ -216,14 +238,16 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
end
|
||||
|
||||
test "handle HTTP 410 Gone response" do
|
||||
assert {:error, "Object has been deleted"} ==
|
||||
assert {:error,
|
||||
{"Object has been deleted", "https://mastodon.example.org/users/userisgone", 410}} ==
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://mastodon.example.org/users/userisgone"
|
||||
)
|
||||
end
|
||||
|
||||
test "handle HTTP 404 response" do
|
||||
assert {:error, "Object has been deleted"} ==
|
||||
assert {:error,
|
||||
{"Object has been deleted", "https://mastodon.example.org/users/userisgone404", 404}} ==
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://mastodon.example.org/users/userisgone404"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Pleroma.User.SearchTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
describe "sanitise_domain/1" do
|
||||
test "should remove url-reserved characters" do
|
||||
examples = [
|
||||
["example.com", "example.com"],
|
||||
["no spaces", "nospaces"],
|
||||
["no@at", "noat"],
|
||||
["dash-is-ok", "dash-is-ok"],
|
||||
["underscore_not_so_much", "underscorenotsomuch"],
|
||||
["no!", "no"],
|
||||
["no?", "no"],
|
||||
["a$b%s^o*l(u)t'e#l<y n>o/t", "absolutelynot"]
|
||||
]
|
||||
|
||||
for [input, expected] <- examples do
|
||||
assert Pleroma.User.Search.sanitise_domain(input) == expected
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2679,4 +2679,74 @@ defmodule Pleroma.UserTest do
|
|||
assert user.ap_id in user3_updated.also_known_as
|
||||
end
|
||||
end
|
||||
|
||||
describe "follow_hashtag/2" do
|
||||
test "should follow a hashtag" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 1
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
|
||||
test "should not follow a hashtag twice" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 1
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
|
||||
test "can follow multiple hashtags" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
other_hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 2
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "unfollow_hashtag/2" do
|
||||
test "should unfollow a hashtag" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 0
|
||||
end
|
||||
|
||||
test "should not error when trying to unfollow a hashtag twice" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -719,6 +719,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "fetch activities for followed hashtags" do
|
||||
test "it should return public activities that reference a given hashtag" do
|
||||
hashtag = insert(:hashtag, name: "tenshi")
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, normally_visible} =
|
||||
CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"})
|
||||
|
||||
{:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
|
||||
{:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
|
||||
{:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
|
||||
{:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([other_user.follower_address], %{
|
||||
followed_hashtags: [hashtag.id]
|
||||
})
|
||||
|
||||
assert length(activities) == 3
|
||||
normal_id = normally_visible.id
|
||||
public_id = public.id
|
||||
unlisted_id = unlisted.id
|
||||
assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch activities in context" do
|
||||
test "retrieves activities that have a given context" do
|
||||
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
||||
|
|
|
@ -19,6 +19,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
|
|||
})
|
||||
|
||||
assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364
|
||||
|
||||
assert {:ok, %{"type" => "Update", "expires_at" => expires_at}} =
|
||||
ActivityExpirationPolicy.filter(%{
|
||||
"id" => @id,
|
||||
"actor" => @local_actor,
|
||||
"type" => "Update",
|
||||
"object" => %{"type" => "Note"}
|
||||
})
|
||||
|
||||
assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364
|
||||
end
|
||||
|
||||
test "keeps existing `expires_at` if it less than the config setting" do
|
||||
|
@ -32,6 +42,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
|
|||
"expires_at" => expires_at,
|
||||
"object" => %{"type" => "Note"}
|
||||
})
|
||||
|
||||
assert {:ok, %{"type" => "Update", "expires_at" => ^expires_at}} =
|
||||
ActivityExpirationPolicy.filter(%{
|
||||
"id" => @id,
|
||||
"actor" => @local_actor,
|
||||
"type" => "Update",
|
||||
"expires_at" => expires_at,
|
||||
"object" => %{"type" => "Note"}
|
||||
})
|
||||
end
|
||||
|
||||
test "overwrites existing `expires_at` if it greater than the config setting" do
|
||||
|
@ -47,6 +66,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
|
|||
})
|
||||
|
||||
assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364
|
||||
|
||||
assert {:ok, %{"type" => "Update", "expires_at" => expires_at}} =
|
||||
ActivityExpirationPolicy.filter(%{
|
||||
"id" => @id,
|
||||
"actor" => @local_actor,
|
||||
"type" => "Update",
|
||||
"expires_at" => too_distant_future,
|
||||
"object" => %{"type" => "Note"}
|
||||
})
|
||||
|
||||
assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364
|
||||
end
|
||||
|
||||
test "ignores remote activities" do
|
||||
|
@ -59,9 +89,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
|
|||
})
|
||||
|
||||
refute Map.has_key?(activity, "expires_at")
|
||||
|
||||
assert {:ok, activity} =
|
||||
ActivityExpirationPolicy.filter(%{
|
||||
"id" => "https://example.com/123",
|
||||
"actor" => "https://example.com/users/cofe",
|
||||
"type" => "Update",
|
||||
"object" => %{"type" => "Note"}
|
||||
})
|
||||
|
||||
refute Map.has_key?(activity, "expires_at")
|
||||
end
|
||||
|
||||
test "ignores non-Create/Note activities" do
|
||||
test "ignores non-Create/Update/Note activities" do
|
||||
assert {:ok, activity} =
|
||||
ActivityExpirationPolicy.filter(%{
|
||||
"id" => "https://example.com/123",
|
||||
|
|
|
@ -32,6 +32,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
}
|
||||
}
|
||||
|
||||
@linkless_update_message %{
|
||||
"type" => "Update",
|
||||
"object" => %{
|
||||
"content" => "hi world!"
|
||||
}
|
||||
}
|
||||
|
||||
@linkful_update_message %{
|
||||
"type" => "Update",
|
||||
"object" => %{
|
||||
"content" => "<a href='https://example.com'>hi world!</a>"
|
||||
}
|
||||
}
|
||||
|
||||
@response_update_message %{
|
||||
"type" => "Update",
|
||||
"object" => %{
|
||||
"name" => "yes",
|
||||
"type" => "Answer"
|
||||
}
|
||||
}
|
||||
|
||||
describe "with new user" do
|
||||
test "it allows posts without links" do
|
||||
user = insert(:user, local: false)
|
||||
|
@ -42,7 +64,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkless_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkless_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
|
||||
test "it disallows posts with links" do
|
||||
|
@ -66,7 +93,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
}
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"actor" => user.ap_id,
|
||||
"object" => %{
|
||||
"formerRepresentations" => %{
|
||||
"type" => "OrderedCollection",
|
||||
"orderedItems" => [
|
||||
%{
|
||||
"content" => "<a href='https://example.com'>hi world!</a>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"content" => "mew"
|
||||
}
|
||||
}
|
||||
|
||||
{:reject, _} = MRF.filter_one(AntiLinkSpamPolicy, message)
|
||||
{:reject, _} = MRF.filter_one(AntiLinkSpamPolicy, update_message)
|
||||
end
|
||||
|
||||
test "it allows posts with links for local users" do
|
||||
|
@ -78,7 +122,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkful_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkful_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
|
||||
test "it disallows posts with links in history" do
|
||||
|
@ -90,7 +139,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkful_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkful_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(message)
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -104,7 +158,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkless_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkless_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
|
||||
test "it allows posts with links" do
|
||||
|
@ -116,7 +175,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkful_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkful_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -130,7 +194,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkless_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkless_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
|
||||
test "it allows posts with links" do
|
||||
|
@ -142,7 +211,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkful_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@linkful_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -161,9 +235,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkless_message
|
||||
|> Map.put("actor", "http://invalid.actor")
|
||||
|
||||
update_message =
|
||||
@linkless_update_message
|
||||
|> Map.put("actor", "http://invalid.actor")
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(message)
|
||||
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
|
||||
end
|
||||
|
||||
test "it rejects posts with links" do
|
||||
|
@ -171,9 +253,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@linkful_message
|
||||
|> Map.put("actor", "http://invalid.actor")
|
||||
|
||||
update_message =
|
||||
@linkful_update_message
|
||||
|> Map.put("actor", "http://invalid.actor")
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(message)
|
||||
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:reject, _} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -185,7 +275,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
|
|||
@response_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
update_message =
|
||||
@response_update_message
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|
||||
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
|
||||
{:ok, _update_message} = AntiLinkSpamPolicy.filter(update_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicyTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF.FollowBotPolicy
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "FollowBotPolicy" do
|
||||
test "follows remote users" do
|
||||
bot = insert(:user, actor_type: "Service")
|
||||
remote_user = insert(:user, local: false)
|
||||
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"to" => [remote_user.follower_address],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"content" => "Test post",
|
||||
"type" => "Note",
|
||||
"attributedTo" => remote_user.ap_id,
|
||||
"inReplyTo" => nil
|
||||
},
|
||||
"actor" => remote_user.ap_id
|
||||
}
|
||||
|
||||
refute User.following?(bot, remote_user)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 0
|
||||
|
||||
FollowBotPolicy.filter(message)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 1
|
||||
end
|
||||
|
||||
test "does not follow users with #nobot in bio" do
|
||||
bot = insert(:user, actor_type: "Service")
|
||||
remote_user = insert(:user, %{local: false, bio: "go away bots! #nobot"})
|
||||
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"to" => [remote_user.follower_address],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"content" => "I don't like follow bots",
|
||||
"type" => "Note",
|
||||
"attributedTo" => remote_user.ap_id,
|
||||
"inReplyTo" => nil
|
||||
},
|
||||
"actor" => remote_user.ap_id
|
||||
}
|
||||
|
||||
refute User.following?(bot, remote_user)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 0
|
||||
|
||||
FollowBotPolicy.filter(message)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 0
|
||||
end
|
||||
|
||||
test "does not follow local users" do
|
||||
bot = insert(:user, actor_type: "Service")
|
||||
local_user = insert(:user, local: true)
|
||||
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"to" => [local_user.follower_address],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"content" => "Hi I'm a local user",
|
||||
"type" => "Note",
|
||||
"attributedTo" => local_user.ap_id,
|
||||
"inReplyTo" => nil
|
||||
},
|
||||
"actor" => local_user.ap_id
|
||||
}
|
||||
|
||||
refute User.following?(bot, local_user)
|
||||
|
||||
assert User.get_follow_requests(local_user) |> length == 0
|
||||
|
||||
FollowBotPolicy.filter(message)
|
||||
|
||||
assert User.get_follow_requests(local_user) |> length == 0
|
||||
end
|
||||
|
||||
test "does not follow users requiring follower approval" do
|
||||
bot = insert(:user, actor_type: "Service")
|
||||
remote_user = insert(:user, %{local: false, is_locked: true})
|
||||
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"to" => [remote_user.follower_address],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"content" => "I don't like randos following me",
|
||||
"type" => "Note",
|
||||
"attributedTo" => remote_user.ap_id,
|
||||
"inReplyTo" => nil
|
||||
},
|
||||
"actor" => remote_user.ap_id
|
||||
}
|
||||
|
||||
refute User.following?(bot, remote_user)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 0
|
||||
|
||||
FollowBotPolicy.filter(message)
|
||||
|
||||
assert User.get_follow_requests(remote_user) |> length == 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,35 +26,60 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do
|
|||
}}
|
||||
end
|
||||
|
||||
defp generate_update_messages(actor) do
|
||||
{%{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{},
|
||||
"to" => [@public, "f"],
|
||||
"cc" => [actor.follower_address, "d"]
|
||||
},
|
||||
%{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]},
|
||||
"to" => ["f", actor.follower_address],
|
||||
"cc" => ["d", @public]
|
||||
}}
|
||||
end
|
||||
|
||||
test "removes from the federated timeline by nickname heuristics 1" do
|
||||
actor = insert(:user, %{nickname: "annoying_ebooks@example.com"})
|
||||
|
||||
{message, except_message} = generate_messages(actor)
|
||||
{update_message, except_update_message} = generate_update_messages(actor)
|
||||
|
||||
assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message}
|
||||
assert ForceBotUnlistedPolicy.filter(update_message) == {:ok, except_update_message}
|
||||
end
|
||||
|
||||
test "removes from the federated timeline by nickname heuristics 2" do
|
||||
actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"})
|
||||
|
||||
{message, except_message} = generate_messages(actor)
|
||||
{update_message, except_update_message} = generate_update_messages(actor)
|
||||
|
||||
assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message}
|
||||
assert ForceBotUnlistedPolicy.filter(update_message) == {:ok, except_update_message}
|
||||
end
|
||||
|
||||
test "removes from the federated timeline by actor type Application" do
|
||||
actor = insert(:user, %{actor_type: "Application"})
|
||||
|
||||
{message, except_message} = generate_messages(actor)
|
||||
{update_message, except_update_message} = generate_update_messages(actor)
|
||||
|
||||
assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message}
|
||||
assert ForceBotUnlistedPolicy.filter(update_message) == {:ok, except_update_message}
|
||||
end
|
||||
|
||||
test "removes from the federated timeline by actor type Service" do
|
||||
actor = insert(:user, %{actor_type: "Service"})
|
||||
|
||||
{message, except_message} = generate_messages(actor)
|
||||
{update_message, except_update_message} = generate_update_messages(actor)
|
||||
|
||||
assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message}
|
||||
assert ForceBotUnlistedPolicy.filter(update_message) == {:ok, except_update_message}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,54 +26,86 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
|
|||
}
|
||||
}
|
||||
|
||||
[user: user, message: message]
|
||||
update_message = %{
|
||||
"actor" => user.ap_id,
|
||||
"cc" => [user.follower_address],
|
||||
"type" => "Update",
|
||||
"to" => [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://instance.tld/users/user1",
|
||||
"https://instance.tld/users/user2",
|
||||
"https://instance.tld/users/user3"
|
||||
],
|
||||
"object" => %{
|
||||
"type" => "Note"
|
||||
}
|
||||
}
|
||||
|
||||
[user: user, message: message, update_message: update_message]
|
||||
end
|
||||
|
||||
setup do: clear_config(:mrf_hellthread)
|
||||
|
||||
describe "reject" do
|
||||
test "rejects the message if the recipient count is above reject_threshold", %{
|
||||
message: message
|
||||
message: message,
|
||||
update_message: update_message
|
||||
} do
|
||||
clear_config([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2})
|
||||
|
||||
assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} ==
|
||||
filter(message)
|
||||
|
||||
assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} ==
|
||||
filter(update_message)
|
||||
end
|
||||
|
||||
test "does not reject the message if the recipient count is below reject_threshold", %{
|
||||
message: message
|
||||
message: message,
|
||||
update_message: update_message
|
||||
} do
|
||||
clear_config([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3})
|
||||
|
||||
assert {:ok, ^message} = filter(message)
|
||||
assert {:ok, ^update_message} = filter(update_message)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delist" do
|
||||
test "delists the message if the recipient count is above delist_threshold", %{
|
||||
user: user,
|
||||
message: message
|
||||
message: message,
|
||||
update_message: update_message
|
||||
} do
|
||||
clear_config([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0})
|
||||
|
||||
{:ok, message} = filter(message)
|
||||
assert user.follower_address in message["to"]
|
||||
assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"]
|
||||
|
||||
{:ok, update_message} = filter(update_message)
|
||||
assert user.follower_address in update_message["to"]
|
||||
assert "https://www.w3.org/ns/activitystreams#Public" in update_message["cc"]
|
||||
end
|
||||
|
||||
test "does not delist the message if the recipient count is below delist_threshold", %{
|
||||
message: message
|
||||
message: message,
|
||||
update_message: update_message
|
||||
} do
|
||||
clear_config([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0})
|
||||
|
||||
assert {:ok, ^message} = filter(message)
|
||||
assert {:ok, ^update_message} = filter(update_message)
|
||||
end
|
||||
end
|
||||
|
||||
test "excludes follower collection and public URI from threshold count", %{message: message} do
|
||||
test "excludes follower collection and public URI from threshold count", %{
|
||||
message: message,
|
||||
update_message: update_message
|
||||
} do
|
||||
clear_config([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3})
|
||||
|
||||
assert {:ok, ^message} = filter(message)
|
||||
assert {:ok, ^update_message} = filter(update_message)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"cc" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"to" => ["https://example.com/ok"],
|
||||
"cc" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) == {:ok, message}
|
||||
assert MentionPolicy.filter(update_message) == {:ok, update_message}
|
||||
end
|
||||
|
||||
describe "allow" do
|
||||
|
@ -29,7 +36,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"type" => "Create"
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update"
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) == {:ok, message}
|
||||
assert MentionPolicy.filter(update_message) == {:ok, update_message}
|
||||
end
|
||||
|
||||
test "to" do
|
||||
|
@ -40,7 +52,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"to" => ["https://example.com/ok"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"to" => ["https://example.com/ok"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) == {:ok, message}
|
||||
assert MentionPolicy.filter(update_message) == {:ok, update_message}
|
||||
end
|
||||
|
||||
test "cc" do
|
||||
|
@ -51,7 +69,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"cc" => ["https://example.com/ok"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"cc" => ["https://example.com/ok"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) == {:ok, message}
|
||||
assert MentionPolicy.filter(update_message) == {:ok, update_message}
|
||||
end
|
||||
|
||||
test "both" do
|
||||
|
@ -63,7 +87,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"cc" => ["https://example.com/ok2"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"to" => ["https://example.com/ok"],
|
||||
"cc" => ["https://example.com/ok2"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) == {:ok, message}
|
||||
assert MentionPolicy.filter(update_message) == {:ok, update_message}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -76,8 +107,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"to" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"to" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) ==
|
||||
{:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"}
|
||||
|
||||
assert MentionPolicy.filter(update_message) ==
|
||||
{:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"}
|
||||
end
|
||||
|
||||
test "cc" do
|
||||
|
@ -89,8 +128,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
|
|||
"cc" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
update_message = %{
|
||||
"type" => "Update",
|
||||
"to" => ["https://example.com/ok"],
|
||||
"cc" => ["https://example.com/blocked"]
|
||||
}
|
||||
|
||||
assert MentionPolicy.filter(message) ==
|
||||
{:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"}
|
||||
|
||||
assert MentionPolicy.filter(update_message) ==
|
||||
{:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,15 +26,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
test "is empty" do
|
||||
clear_config([:mrf_simple, :media_removal], [])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) == {:ok, media_message}
|
||||
assert SimplePolicy.filter(media_update_message) == {:ok, media_update_message}
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
|
||||
test "has a matching host" do
|
||||
clear_config([:mrf_simple, :media_removal], [{"remote.instance", "Some reason"}])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) ==
|
||||
|
@ -42,12 +45,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
media_message
|
||||
|> Map.put("object", Map.delete(media_message["object"], "attachment"))}
|
||||
|
||||
assert SimplePolicy.filter(media_update_message) ==
|
||||
{:ok,
|
||||
media_update_message
|
||||
|> Map.put("object", Map.delete(media_update_message["object"], "attachment"))}
|
||||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
|
||||
test "match with wildcard domain" do
|
||||
clear_config([:mrf_simple, :media_removal], [{"*.remote.instance", "Whatever reason"}])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) ==
|
||||
|
@ -55,6 +64,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
media_message
|
||||
|> Map.put("object", Map.delete(media_message["object"], "attachment"))}
|
||||
|
||||
assert SimplePolicy.filter(media_update_message) ==
|
||||
{:ok,
|
||||
media_update_message
|
||||
|> Map.put("object", Map.delete(media_update_message["object"], "attachment"))}
|
||||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
end
|
||||
|
@ -63,31 +77,41 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
test "is empty" do
|
||||
clear_config([:mrf_simple, :media_nsfw], [])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) == {:ok, media_message}
|
||||
assert SimplePolicy.filter(media_update_message) == {:ok, media_update_message}
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
|
||||
test "has a matching host" do
|
||||
clear_config([:mrf_simple, :media_nsfw], [{"remote.instance", "Whetever"}])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) ==
|
||||
{:ok, put_in(media_message, ["object", "sensitive"], true)}
|
||||
|
||||
assert SimplePolicy.filter(media_update_message) ==
|
||||
{:ok, put_in(media_update_message, ["object", "sensitive"], true)}
|
||||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
|
||||
test "match with wildcard domain" do
|
||||
clear_config([:mrf_simple, :media_nsfw], [{"*.remote.instance", "yeah yeah"}])
|
||||
media_message = build_media_message()
|
||||
media_update_message = build_media_update_message()
|
||||
local_message = build_local_message()
|
||||
|
||||
assert SimplePolicy.filter(media_message) ==
|
||||
{:ok, put_in(media_message, ["object", "sensitive"], true)}
|
||||
|
||||
assert SimplePolicy.filter(media_update_message) ==
|
||||
{:ok, put_in(media_update_message, ["object", "sensitive"], true)}
|
||||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
end
|
||||
end
|
||||
|
@ -104,6 +128,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
}
|
||||
end
|
||||
|
||||
defp build_media_update_message do
|
||||
%{
|
||||
"actor" => "https://remote.instance/users/bob",
|
||||
"type" => "Update",
|
||||
"object" => %{
|
||||
"attachment" => [%{}],
|
||||
"tag" => ["foo"],
|
||||
"sensitive" => false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe "when :report_removal" do
|
||||
test "is empty" do
|
||||
clear_config([:mrf_simple, :report_removal], [])
|
||||
|
@ -320,6 +356,86 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
|
||||
assert {:reject, _} = SimplePolicy.filter(announce)
|
||||
end
|
||||
|
||||
test "accept by matching context URI if :handle_threads is disabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], false)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put("context", "https://blocked.tld/contexts/abc")
|
||||
|
||||
assert {:ok, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
|
||||
test "accept by matching conversation field if :handle_threads is disabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], false)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put(
|
||||
"conversation",
|
||||
"tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
|
||||
)
|
||||
|
||||
assert {:ok, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
|
||||
test "accept by matching reply ID if :handle_threads is disabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], false)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("object", %{
|
||||
"type" => "Note",
|
||||
"inReplyTo" => "https://blocked.tld/objects/1"
|
||||
})
|
||||
|
||||
assert {:ok, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
|
||||
test "reject by matching context URI if :handle_threads is enabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], true)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put("context", "https://blocked.tld/contexts/abc")
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
|
||||
test "reject by matching conversation field if :handle_threads is enabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], true)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put(
|
||||
"conversation",
|
||||
"tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
|
||||
)
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
|
||||
test "reject by matching reply ID if :handle_threads is enabled" do
|
||||
clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
|
||||
clear_config([:mrf_simple, :handle_threads], true)
|
||||
|
||||
remote_message =
|
||||
build_remote_message()
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("object", %{
|
||||
"type" => "Note",
|
||||
"inReplyTo" => "https://blocked.tld/objects/1"
|
||||
})
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when :followers_only" do
|
||||
|
|
|
@ -53,7 +53,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
|
|||
"cc" => ["d"]
|
||||
}
|
||||
|
||||
edit_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{},
|
||||
"to" => [@public, "f"],
|
||||
"cc" => [@public, "d"]
|
||||
}
|
||||
|
||||
edit_expect_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"to" => ["f", actor.follower_address], "cc" => ["d"]},
|
||||
"to" => ["f", actor.follower_address],
|
||||
"cc" => ["d"]
|
||||
}
|
||||
|
||||
assert TagPolicy.filter(message) == {:ok, except_message}
|
||||
assert TagPolicy.filter(edit_message) == {:ok, edit_expect_message}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +94,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
|
|||
"cc" => ["d", @public]
|
||||
}
|
||||
|
||||
edit_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{},
|
||||
"to" => [@public, "f"],
|
||||
"cc" => [actor.follower_address, "d"]
|
||||
}
|
||||
|
||||
edit_expect_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]},
|
||||
"to" => ["f", actor.follower_address],
|
||||
"cc" => ["d", @public]
|
||||
}
|
||||
|
||||
assert TagPolicy.filter(message) == {:ok, except_message}
|
||||
assert TagPolicy.filter(edit_message) == {:ok, edit_expect_message}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -97,7 +131,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
|
|||
"object" => %{}
|
||||
}
|
||||
|
||||
edit_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"attachment" => ["file1"]}
|
||||
}
|
||||
|
||||
edit_expect_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{}
|
||||
}
|
||||
|
||||
assert TagPolicy.filter(message) == {:ok, except_message}
|
||||
assert TagPolicy.filter(edit_message) == {:ok, edit_expect_message}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -117,7 +164,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
|
|||
"object" => %{"tag" => ["test"], "attachment" => ["file1"], "sensitive" => true}
|
||||
}
|
||||
|
||||
edit_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"tag" => ["test"], "attachment" => ["file1"]}
|
||||
}
|
||||
|
||||
edit_expect_message = %{
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "Update",
|
||||
"object" => %{"tag" => ["test"], "attachment" => ["file1"], "sensitive" => true}
|
||||
}
|
||||
|
||||
assert TagPolicy.filter(message) == {:ok, except_message}
|
||||
assert TagPolicy.filter(edit_message) == {:ok, edit_expect_message}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.TagControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
import Pleroma.Factory
|
||||
import Tesla.Mock
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
setup do
|
||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "GET /api/v1/tags/:id" do
|
||||
test "returns 200 with tag" do
|
||||
%{user: user, conn: conn} = oauth_access(["read"])
|
||||
|
||||
tag = insert(:hashtag, name: "jubjub")
|
||||
{:ok, _user} = User.follow_hashtag(user, tag)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/tags/jubjub")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert %{
|
||||
"name" => "jubjub",
|
||||
"url" => "http://localhost:4001/tags/jubjub",
|
||||
"history" => [],
|
||||
"following" => true
|
||||
} = response
|
||||
end
|
||||
|
||||
test "returns 404 with unknown tag" do
|
||||
%{conn: conn} = oauth_access(["read"])
|
||||
|
||||
conn
|
||||
|> get("/api/v1/tags/jubjub")
|
||||
|> json_response_and_validate_schema(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/tags/:id/follow" do
|
||||
test "should follow a hashtag" do
|
||||
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||
hashtag = insert(:hashtag, name: "jubjub")
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/tags/jubjub/follow")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response["following"] == true
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
assert User.following_hashtag?(user, hashtag)
|
||||
end
|
||||
|
||||
test "should 404 if hashtag doesn't exist" do
|
||||
%{conn: conn} = oauth_access(["write:follows"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/tags/rubrub/follow")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert response["error"] == "Hashtag not found"
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/tags/:id/unfollow" do
|
||||
test "should unfollow a hashtag" do
|
||||
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||
hashtag = insert(:hashtag, name: "jubjub")
|
||||
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/tags/jubjub/unfollow")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response["following"] == false
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
refute User.following_hashtag?(user, hashtag)
|
||||
end
|
||||
|
||||
test "should 404 if hashtag doesn't exist" do
|
||||
%{conn: conn} = oauth_access(["write:follows"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/tags/rubrub/unfollow")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert response["error"] == "Hashtag not found"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
@ -42,8 +44,67 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">public<"
|
||||
refute html =~ ">private<"
|
||||
assert html =~ "\npublic\n"
|
||||
refute html =~ "\nprivate\n"
|
||||
end
|
||||
|
||||
test "main page does not include replies", %{conn: conn, user: user} do
|
||||
{:ok, op} = CommonAPI.post(user, %{status: "beep"})
|
||||
CommonAPI.post(user, %{status: "boop", in_reply_to_id: op})
|
||||
|
||||
conn = get(conn, "/users/#{user.nickname}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ "\nbeep\n"
|
||||
refute html =~ "\nboop\n"
|
||||
end
|
||||
|
||||
test "media page only includes posts with attachments", %{conn: conn, user: user} do
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
CommonAPI.post(user, %{status: "virgin text post"})
|
||||
CommonAPI.post(user, %{status: "chad post with attachment", media_ids: [media_id]})
|
||||
|
||||
conn = get(conn, "/users/#{user.nickname}/media")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ "\nchad post with attachment\n"
|
||||
refute html =~ "\nvirgin text post\n"
|
||||
end
|
||||
|
||||
test "show follower list", %{conn: conn, user: user} do
|
||||
follower = insert(:user)
|
||||
CommonAPI.follow(follower, user)
|
||||
|
||||
conn = get(conn, "/users/#{user.nickname}/followers")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ "user-card"
|
||||
end
|
||||
|
||||
test "don't show followers if hidden", %{conn: conn, user: user} do
|
||||
follower = insert(:user)
|
||||
CommonAPI.follow(follower, user)
|
||||
|
||||
{:ok, user} =
|
||||
user
|
||||
|> User.update_changeset(%{hide_followers: true})
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
conn = get(conn, "/users/#{user.nickname}/followers")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
refute html =~ "user-card"
|
||||
end
|
||||
|
||||
test "pagination", %{conn: conn, user: user} do
|
||||
|
@ -53,10 +114,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">test30<"
|
||||
assert html =~ ">test11<"
|
||||
refute html =~ ">test10<"
|
||||
refute html =~ ">test1<"
|
||||
assert html =~ "\ntest30\n"
|
||||
assert html =~ "\ntest11\n"
|
||||
refute html =~ "\ntest10\n"
|
||||
refute html =~ "\ntest1\n"
|
||||
end
|
||||
|
||||
test "pagination, page 2", %{conn: conn, user: user} do
|
||||
|
@ -67,10 +128,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">test1<"
|
||||
assert html =~ ">test10<"
|
||||
refute html =~ ">test20<"
|
||||
refute html =~ ">test29<"
|
||||
assert html =~ "\ntest1\n"
|
||||
assert html =~ "\ntest10\n"
|
||||
refute html =~ "\ntest20\n"
|
||||
refute html =~ "\ntest29\n"
|
||||
end
|
||||
|
||||
test "does not require authentication on non-federating instances", %{
|
||||
|
@ -104,7 +165,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
conn = get(conn, "/notice/#{activity.id}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
assert html =~ "<header>"
|
||||
assert html =~ "<div class=\"panel conversation\">"
|
||||
assert html =~ user.nickname
|
||||
assert html =~ "testing a thing!"
|
||||
end
|
||||
|
|
|
@ -410,6 +410,36 @@ defmodule Pleroma.Web.StreamerTest do
|
|||
assert_receive {:render_with_user, _, "status_update.json", ^create, ^stream}
|
||||
refute Streamer.filtered_by_user?(user, edited)
|
||||
end
|
||||
|
||||
test "it streams posts containing followed hashtags on the 'user' stream", %{
|
||||
user: user,
|
||||
token: oauth_token
|
||||
} do
|
||||
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||
other_user = insert(:user)
|
||||
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||
|
||||
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey #tenshi"})
|
||||
|
||||
assert_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||
end
|
||||
|
||||
test "should not stream private posts containing followed hashtags on the 'user' stream", %{
|
||||
user: user,
|
||||
token: oauth_token
|
||||
} do
|
||||
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||
other_user = insert(:user)
|
||||
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||
|
||||
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(other_user, %{status: "hey #tenshi", visibility: "private"})
|
||||
|
||||
refute_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||
end
|
||||
end
|
||||
|
||||
describe "public streams" do
|
||||
|
|
|
@ -716,4 +716,11 @@ defmodule Pleroma.Factory do
|
|||
user: user
|
||||
}
|
||||
end
|
||||
|
||||
def hashtag_factory(params \\ %{}) do
|
||||
%Pleroma.Hashtag{
|
||||
name: "test #{sequence(:hashtag_name, & &1)}"
|
||||
}
|
||||
|> Map.merge(params)
|
||||
end
|
||||
end
|
||||
|
|