Compare commits

...

48 Commits

Author SHA1 Message Date
FloatingGhost 1f0ef94271 Bump versions 2022-12-10 14:50:02 +00:00
floatingghost 24fe692070 Merge pull request 'Don't listen Erlang Port Mapper Daemon (4369/tcp) on 0.0.0.0' (#358) from r3g_5z/akkoma:close-open-ports into develop
Reviewed-on: AkkomaGang/akkoma#358
2022-12-10 14:43:03 +00:00
floatingghost bfcc7404fe Merge pull request 'Add dark and light theme mode to docs, detection, and button' (#360) from r3g_5z/akkoma:docs-dark-mode into develop
Reviewed-on: AkkomaGang/akkoma#360
2022-12-10 14:41:23 +00:00
r3g_5z fbfffccc1d
Add dark and light theme mode to docs, detection, and button
my eyes hurt

Signed-off-by: r3g_5z <june@girlboss.ceo>
2022-12-09 22:51:43 -05:00
r3g_5z 77174acc7b
Don't listen Erlang Port Mapper Daemon (4369/tcp) on 0.0.0.0
Signed-off-by: r3g_5z <june@girlboss.ceo>
2022-12-09 21:36:21 -05:00
floatingghost 59fde45b36 Merge pull request 'Remove unnecessary KillMode=process' (#359) from r3g_5z/akkoma:remove-unnecessary-killmode into develop
Reviewed-on: AkkomaGang/akkoma#359
2022-12-10 00:24:28 +00:00
FloatingGhost 50ee38128b Merge remote-tracking branch 'origin/translations' into develop 2022-12-10 00:10:24 +00:00
r3g_5z 90fce918b2
Remove unnecessary KillMode=process
It's unclear why this is the default as this is highly not recommended.
KillMode=process ends up leaving leftover orphaned processes that
escape resource management and process lifecycles, wasting resources
on servers.

Signed-off-by: r3g_5z <june@girlboss.ceo>
2022-12-09 19:10:20 -05:00
FloatingGhost 68894089e8 Do not fetch anything from blocked instances 2022-12-10 00:09:45 +00:00
FloatingGhost a1515f9a60 Add some extra info around possible nils 2022-12-09 23:45:51 +00:00
Weblate 021b0864a5 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Translation: Pleroma fe/Akkoma Backend (Static pages)
Translate-URL: http://translate.akkoma.dev/projects/akkoma/akkoma-backend-static-pages/
2022-12-09 21:12:53 +00:00
Weblate c33f0065f2 Translated using Weblate (Indonesian)
Currently translated at 21.6% (18 of 83 strings)

Added translation using Weblate (Indonesian)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: t1 <taaa@fedora.email>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/akkoma-backend-static-pages/id/
Translation: Pleroma fe/Akkoma Backend (Static pages)
2022-12-09 21:12:53 +00:00
floatingghost 2144ce5188 Merge pull request 'Magical patches' (#357) from magical-patches into develop
Reviewed-on: AkkomaGang/akkoma#357
2022-12-09 21:12:49 +00:00
FloatingGhost 739ed14f54 Revert "mandate published on notes"
This reverts commit e49b583147.
2022-12-09 20:59:26 +00:00
floatingghost f667884962 Merge pull request 'Skip posts in indexer where publish date is nil' (#356) from sn0w/akkoma:feature/indexer-skip-broken-activities into develop
Reviewed-on: AkkomaGang/akkoma#356
Reviewed-by: floatingghost <hannah@coffee-and-dreams.uk>
2022-12-09 20:28:48 +00:00
FloatingGhost e49b583147 mandate published on notes
fixes #356
2022-12-09 20:27:54 +00:00
FloatingGhost f5a315f04c Add URL and code to :not_found errors
Ref #355
2022-12-09 20:13:31 +00:00
FloatingGhost bc265bfd54 Underscore unused variable 2022-12-09 20:04:48 +00:00
FloatingGhost dcf58a3c53 Do not pass transient undo-y activities through MRF 2022-12-09 20:01:38 +00:00
FloatingGhost 9db4c2429f Remove FollowBotPolicy 2022-12-09 19:59:27 +00:00
FloatingGhost 6f83ae27aa extend reject MRF to check if originating instance is blocked 2022-12-09 19:57:29 +00:00
sn0w 4c0911592b
Skip posts in indexer where publish date is nil 2022-12-09 20:56:39 +01:00
FloatingGhost d5828f1c5e Merge remote-tracking branch 'ilja/fix_tagpolicy_to_also_work_on_updates' into develop 2022-12-09 10:31:22 +00:00
FloatingGhost 0eaec57d3f mix format 2022-12-09 10:24:38 +00:00
ilja 1f863f0a36 Fix MRF policies to also work with Update
Objects who got updated would just pass through several of the MRF policies, undoing moderation in some situations.
In the relevant cases we now check not only for Create activities, but also Update activities.

I checked which ones checked explicitly on type Create using `grep '"type" => "Create"' lib/pleroma/web/activity_pub/mrf/*`.

The following from that list have not been changed:
* lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex
    * Not relevant for moderation
* lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
    * Already had a test for Update
* lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
    * In practice only relevant when fetching old objects (e.g. through Like or Announce). These are always wrapped in a Create.
* lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
    * We don't allow changing scope with Update, so not relevant here
2022-12-08 23:22:05 +01:00
ilja ce517ff4e5 Fix tagpolicy to also work with Update
Objects who got updated would just pass the TagPolicy, undoing the moderation that was set in place for the Actor.
Now we check not only for Create activities, but also Update activities.
2022-12-08 21:53:42 +01:00
floatingghost 9addd8f414 Merge pull request 'Add YAML issue templates for bug and feat' (#353) from sfr/akkoma:issue-template into develop
Reviewed-on: AkkomaGang/akkoma#353
2022-12-08 18:40:45 +00:00
Sol Fisher Romanoff 067bd17e1e
Add YAML issue templates for bug and feat 2022-12-08 20:16:59 +02:00
floatingghost 104d8dcc1f Update 'ISSUE_TEMPLATE.md' 2022-12-07 22:37:23 +00:00
floatingghost 3f1c84d300 Add issue template 2022-12-07 22:27:00 +00:00
FloatingGhost 4e4bd24813 Add misskey markdown to format suggestions
Fixes #345
2022-12-07 15:39:19 +00:00
FloatingGhost cb3ccf5f47 Add check for null reply_to_user 2022-12-07 13:41:12 +00:00
FloatingGhost 1afba64464 Redirect to standard FE if logged in 2022-12-07 13:35:00 +00:00
FloatingGhost 221a95b860 Document custom.css 2022-12-07 11:45:53 +00:00
FloatingGhost c7369d6d03 GOOGLE 2022-12-07 11:41:24 +00:00
sfr 7c4b415929 static-fe overhaul (#236)
makes static-fe look more like pleroma-fe, with the stylesheets matching pleroma-dark and pleroma-light based on `prefers-color-scheme`.

- [x] navbar
- [x] about sidebar
- [x] background image
- [x] statuses
  - [x] "reply to" or "edited" tags
- [x] accounts
  - [x] show more / show less
  - [x] posts / with replies / media / followers / following
    - [x] followers/following would require user card snippets
  - [x] admin/bot indicators
- [x] attachments
  - [x] nsfw attachments
- [x] fontawesome icons
- [x] clean up and sort css
- [x] add pleroma-light
- [x] replace hardcoded strings

also i forgot
- [x] repeated headers

how it looks + sneak peek at statuses:
![](https://akkoma.dev/attachments/c0d3a025-6987-4630-8eb9-5f4db6858359)

Co-authored-by: Sol Fisher Romanoff <sol@solfisher.com>
Reviewed-on: AkkomaGang/akkoma#236
Co-authored-by: sfr <sol@solfisher.com>
Co-committed-by: sfr <sol@solfisher.com>
2022-12-07 11:20:53 +00:00
floatingghost 09326ffa56 Diagnostics tasks (#348)
a bunch of ways to get query plans to help with debugging

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma#348
2022-12-07 11:12:34 +00:00
floatingghost 4f2f2c9125 Merge pull request 'Small improvements to the Gentoo installation isntructions' (#335) from timorl/akkoma:i-use-gentoo-btw into develop
Reviewed-on: AkkomaGang/akkoma#335
2022-12-07 11:07:06 +00:00
ilja fdf33392b3 DOCS: backup restore improvements (#332)
Mostly add how to speed up restoration by adding activities_visibility_index later. Also some small other improvements.

This is based on what I did on a Pleroma instance. I assume the activities_visibility_index taking so long is still true for Akkoma, but can't really test because I don't have a big enough Akkoma DB yet 🙃

Co-authored-by: ilja <git@ilja.space>
Reviewed-on: AkkomaGang/akkoma#332
Reviewed-by: floatingghost <hannah@coffee-and-dreams.uk>
Co-authored-by: ilja <akkoma.dev@ilja.space>
Co-committed-by: ilja <akkoma.dev@ilja.space>
2022-12-07 11:05:35 +00:00
FloatingGhost b058df3faa Allow dashes in domain name search 2022-12-06 10:57:10 +00:00
FloatingGhost 8e5a88edf7 update default favicon 2022-12-05 13:47:52 +00:00
FloatingGhost b70a60c6c5 Doc branding 2022-12-05 13:45:36 +00:00
floatingghost c62e1e3ad5 varnish config/docs (#342)
Co-authored-by: Mark Felder <feld@feld.me>
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma#342
2022-12-05 13:39:27 +00:00
floatingghost d55de5debf Remerge of hashtag following (#341)
this time with less idiot

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma#341
2022-12-05 12:58:48 +00:00
floatingghost ec6bf8c3f7 revert 4a94c9a31e
revert Add ability to follow hashtags (#336)

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma#336
2022-12-04 20:04:09 +00:00
floatingghost 4c3971aebd Add changelog entry for hashtag following 2022-12-04 18:35:04 +00:00
floatingghost 4a94c9a31e Add ability to follow hashtags (#336)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma#336
2022-12-04 17:36:59 +00:00
timorl d923cb96b1
Small improvements to the Gentoo installation isntructions 2022-12-04 16:37:49 +01:00
100 changed files with 7622 additions and 3532 deletions

5
.gitattributes vendored
View File

@ -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

View File

@ -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."

View File

@ -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."

View File

@ -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.

View File

@ -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

View File

@ -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: [],

View File

@ -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,

View File

@ -1,4 +1,5 @@
MIX_ENV=prod
ERL_EPMD_ADDRESS=127.0.0.1
DB_NAME=akkoma
DB_USER=akkoma
DB_PASS=akkoma

104
docs/Pipfile.lock generated
View File

@ -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": {}

View File

@ -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>`

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/docs/images/favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
docs/docs/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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`:

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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";

View File

@ -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"

View File

@ -31,6 +31,7 @@ else
fi
export MIX_ENV=prod
export ERL_EPMD_ADDRESS=127.0.0.1
depend() {
need nginx postgresql

View File

@ -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()
{

View File

@ -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

View File

@ -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 =

View File

@ -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} ->

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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"])
%{

View File

@ -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"])
%{

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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],

View File

@ -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"] || [])

View File

@ -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

View File

@ -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

View File

@ -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
_ ->

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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]),

View File

@ -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(),

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -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 ""

File diff suppressed because it is too large Load Diff

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
)

View File

@ -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

View File

@ -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

View File

@ -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"})

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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