diff --git a/.gitignore b/.gitignore index b62891f26..66d1376c5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,6 @@ erl_crash.dump # variables. /config/*.secret.exs /config/generated_config.exs -/config/*.env - # Database setup file, some may forget to delete it /config/setup_db.psql diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9ab84892..9e9107ce3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,7 @@ stages: - docker before_script: + - apt-get update && apt-get install -y cmake - mix local.hex --force - mix local.rebar --force @@ -193,6 +194,7 @@ amd64: variables: &release-variables MIX_ENV: prod before_script: &before-release + - apt install cmake -y - echo "import Mix.Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -211,7 +213,7 @@ amd64-musl: cache: *release-cache variables: *release-variables before_script: &before-release-musl - - apk add git gcc g++ musl-dev make + - apk add git gcc g++ musl-dev make cmake - echo "import Mix.Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force diff --git a/CHANGELOG.md b/CHANGELOG.md index 129c269aa..a8e80eb3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- **Breaking:** Added the ObjectAgePolicy to the default set of MRFs. This will delist and strip the follower collection of any message received that is older than 7 days. This will stop users from seeing very old messages in the timelines. The messages can still be viewed on the user's page and in conversations. They also still trigger notifications. - **Breaking:** Elixir >=1.9 is now required (was >= 1.8) - **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated. - In Conversations, return only direct messages as `last_status` @@ -15,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. - Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated. - **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated. +- **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated.
API Changes @@ -49,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Configuration: Added a blacklist for email servers. - Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation. - Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. @@ -69,7 +72,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for viewing instances favicons next to posts and accounts - Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata. - "By approval" registrations mode. -- Configuration: Added `:welcome` settings for the welcome message to newly registered users. +- Configuration: Added `:welcome` settings for the welcome message to newly registered users. You can send a welcome message as a direct message, chat or email. - Ability to hide favourites and emoji reactions in the API with `[:instance, :show_reactions]` config.
@@ -101,6 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix CSP policy generation to include remote Captcha services - Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance. - Emoji Packs could not be listed when instance was set to `public: false` +- Fix whole_word always returning false on filter get requests ## [Unreleased (patch)] diff --git a/Dockerfile b/Dockerfile index 0f4fcd0bb..aa50e27ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ COPY . . ENV MIX_ENV=prod -RUN apk add git gcc g++ musl-dev make &&\ +RUN apk add git gcc g++ musl-dev make cmake &&\ echo "import Mix.Config" > config/prod.secret.exs &&\ mix local.hex --force &&\ mix local.rebar --force &&\ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..c212a2505 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Pleroma backend security policy + +## Supported versions + +Currently, Pleroma offers bugfixes and security patches only for the latest minor release. + +| Version | Support +|---------| -------- +| 2.0 | Bugfixes and security patches + +## Reporting a vulnerability + +Please use confidential issues (tick the "This issue is confidential and should only be visible to team members with at least Reporter access." box when submitting) at our [bugtracker](https://git.pleroma.social/pleroma/pleroma/-/issues/new) for reporting vulnerabilities. +## Announcements + +New releases are announced at [pleroma.social](https://pleroma.social/announcements/). All security releases are tagged with ["Security"](https://pleroma.social/announcements/tags/security/). You can be notified of them by subscribing to an Atom feed at . diff --git a/config/config.exs b/config/config.exs index 8cf7f169e..5c223c6b6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -264,6 +264,11 @@ sender_nickname: nil, message: nil ], + chat_message: [ + enabled: false, + sender_nickname: nil, + message: nil + ], email: [ enabled: false, sender: nil, @@ -377,6 +382,7 @@ federated_timeline_removal: [], report_removal: [], reject: [], + followers_only: [], accept: [], avatar_removal: [], banner_removal: [], @@ -395,8 +401,9 @@ accept: [], reject: [] +# threshold of 7 days config :pleroma, :mrf_object_age, - threshold: 172_800, + threshold: 604_800, actions: [:delist, :strip_followers] config :pleroma, :rich_media, @@ -511,8 +518,15 @@ "user-search", "user_exists", "users", - "web" - ] + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ], + email_blacklist: [] config :pleroma, Oban, repo: Pleroma.Repo, @@ -722,7 +736,7 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false config :pleroma, :mrf, - policies: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, + policies: Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, transparency: true, transparency_exclusions: [] @@ -732,6 +746,10 @@ config :pleroma, :instances_favicons, enabled: false +config :floki, :html_parser, Floki.HTMLParser.FastHtml + +config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index 11fbe0d78..7734ff7a1 100644 --- a/config/description.exs +++ b/config/description.exs @@ -194,7 +194,7 @@ type: [:string, {:list, :string}, {:list, :tuple}], description: "List of actions for the mogrify command. It's possible to add self-written settings as string. " <> - "For example `[\"auto-orient\", \"strip\", {\"resize\", \"3840x1080>\"}]` string will be parsed into list of the settings.", + "For example `auto-orient, strip, {\"resize\", \"3840x1080>\"}` value will be parsed into valid list of the settings.", suggestions: [ "strip", "auto-orient", @@ -951,7 +951,7 @@ }, %{ key: :instance_thumbnail, - type: :string, + type: {:string, :image}, description: "The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance.", suggestions: ["/instance/thumbnail.jpeg"] @@ -964,25 +964,25 @@ ] }, %{ - group: :welcome, + group: :pleroma, + key: :welcome, type: :group, description: "Welcome messages settings", children: [ %{ - group: :direct_message, - type: :group, + key: :direct_message, + type: :keyword, descpiption: "Direct message settings", children: [ %{ key: :enabled, type: :boolean, - description: "Enables sends direct message for new user after registration" + description: "Enables sending a direct message to newly registered users" }, %{ key: :message, type: :string, - description: - "A message that will be sent to a newly registered users as a direct message", + description: "A message that will be sent to newly registered users", suggestions: [ "Hi, @username! Welcome on board!" ] @@ -990,7 +990,7 @@ %{ key: :sender_nickname, type: :string, - description: "The nickname of the local user that sends the welcome message", + description: "The nickname of the local user that sends a welcome message", suggestions: [ "lain" ] @@ -998,20 +998,49 @@ ] }, %{ - group: :email, - type: :group, + key: :chat_message, + type: :keyword, + descpiption: "Chat message settings", + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Enables sending a chat message to newly registered users" + }, + %{ + key: :message, + type: :string, + description: + "A message that will be sent to newly registered users as a chat message", + suggestions: [ + "Hello, welcome on board!" + ] + }, + %{ + key: :sender_nickname, + type: :string, + description: "The nickname of the local user that sends a welcome chat message", + suggestions: [ + "lain" + ] + } + ] + }, + %{ + key: :email, + type: :keyword, descpiption: "Email message settings", children: [ %{ key: :enabled, type: :boolean, - description: "Enables sends direct message for new user after registration" + description: "Enables sending an email to newly registered users" }, %{ key: :sender, type: [:string, :tuple], description: - "The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.", + "Email address and/or nickname that will be used to send the welcome email.", suggestions: [ {"Pleroma App", "welcome@pleroma.app"} ] @@ -1020,21 +1049,21 @@ key: :subject, type: :string, description: - "The subject of welcome email. Can be use EEX template with `user` and `instance_name` variables.", + "Subject of the welcome email. EEX template with user and instance_name variables can be used.", suggestions: ["Welcome to <%= instance_name%>"] }, %{ key: :html, type: :string, description: - "The html content of welcome email. Can be use EEX template with `user` and `instance_name` variables.", + "HTML content of the welcome email. EEX template with user and instance_name variables can be used.", suggestions: ["

Hello <%= user.name%>. Welcome to <%= instance_name%>

"] }, %{ key: :text, type: :string, description: - "The text content of welcome email. Can be use EEX template with `user` and `instance_name` variables.", + "Text content of the welcome email. EEX template with user and instance_name variables can be used.", suggestions: ["Hello <%= user.name%>. \n Welcome to <%= instance_name%>\n"] } ] @@ -1207,7 +1236,7 @@ }, %{ key: :background, - type: :string, + type: {:string, :image}, description: "URL of the background, unless viewing a user profile with a background that is set", suggestions: ["/images/city.jpg"] @@ -1264,7 +1293,7 @@ }, %{ key: :logo, - type: :string, + type: {:string, :image}, description: "URL of the logo, defaults to Pleroma's logo", suggestions: ["/static/logo.png"] }, @@ -1296,7 +1325,7 @@ %{ key: :nsfwCensorImage, label: "NSFW Censor Image", - type: :string, + type: {:string, :image}, description: "URL of the image to use for hiding NSFW media attachments in the timeline", suggestions: ["/static/img/nsfw.74818f9.png"] @@ -1422,7 +1451,7 @@ }, %{ key: :default_user_avatar, - type: :string, + type: {:string, :image}, description: "URL of the default user avatar", suggestions: ["/images/avi.png"] } @@ -1542,6 +1571,12 @@ description: "List of instances to only accept activities from (except deletes)", suggestions: ["example.com", "*.example.com"] }, + %{ + key: :followers_only, + type: {:list, :string}, + description: "Force posts from the given instances to be visible by followers only", + suggestions: ["example.com", "*.example.com"] + }, %{ key: :report_removal, type: {:list, :string}, @@ -2607,7 +2642,7 @@ children: [ %{ key: :logo, - type: :string, + type: {:string, :image}, description: "A path to a custom logo. Set it to `nil` to use the default Pleroma logo.", suggestions: ["some/path/logo.png"] }, @@ -3021,6 +3056,7 @@ %{ key: :restricted_nicknames, type: {:list, :string}, + description: "List of nicknames users may not register with.", suggestions: [ ".well-known", "~", @@ -3053,6 +3089,12 @@ "users", "web" ] + }, + %{ + key: :email_blacklist, + type: {:list, :string}, + description: "List of email domains users may not register with.", + suggestions: ["mailinator.com", "maildrop.cc"] } ] }, @@ -3257,13 +3299,13 @@ group: :pleroma, key: :connections_pool, type: :group, - description: "Advanced settings for `gun` connections pool", + description: "Advanced settings for `Gun` connections pool", children: [ %{ key: :connection_acquisition_wait, type: :integer, description: - "Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. Default: 250ms.", + "Timeout to acquire a connection from pool. The total max time is this value multiplied by the number of retries. Default: 250ms.", suggestions: [250] }, %{ @@ -3298,7 +3340,7 @@ group: :pleroma, key: :pools, type: :group, - description: "Advanced settings for `gun` workers pools", + description: "Advanced settings for `Gun` workers pools", children: Enum.map([:federation, :media, :upload, :default], fn pool_name -> %{ @@ -3327,7 +3369,7 @@ group: :pleroma, key: :hackney_pools, type: :group, - description: "Advanced settings for `hackney` connections pools", + description: "Advanced settings for `Hackney` connections pools", children: [ %{ key: :federation, @@ -3391,6 +3433,7 @@ %{ group: :pleroma, key: :restrict_unauthenticated, + label: "Restrict Unauthenticated", type: :group, description: "Disallow viewing timelines, user profiles and statuses for unauthenticated users.", @@ -3513,13 +3556,17 @@ children: [ %{ key: "name", + label: "Name", type: :string, - description: "Name of the installed primary frontend" + description: + "Name of the installed primary frontend. Valid config must include both `Name` and `Reference` values." }, %{ key: "ref", + label: "Reference", type: :string, - description: "reference of the installed primary frontend to be used" + description: + "Reference of the installed primary frontend to be used. Valid config must include both `Name` and `Reference` values." } ] } diff --git a/config/test.exs b/config/test.exs index db0655e73..413c7f0b9 100644 --- a/config/test.exs +++ b/config/test.exs @@ -120,6 +120,8 @@ config :tzdata, :autoupdate, :disabled +config :pleroma, :mrf, policies: [] + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md index 647f6f274..64dd66c0c 100644 --- a/docs/administration/CLI_tasks/database.md +++ b/docs/administration/CLI_tasks/database.md @@ -97,4 +97,14 @@ but should only be run if necessary. **It is safe to cancel this.** ```sh tab="From Source" mix pleroma.database vacuum full -``` \ No newline at end of file +``` + +## Add expiration to all local statuses + +```sh tab="OTP" +./bin/pleroma_ctl database ensure_expiration +``` + +```sh tab="From Source" +mix pleroma.database ensure_expiration +``` diff --git a/docs/administration/CLI_tasks/release_environments.md b/docs/administration/CLI_tasks/release_environments.md deleted file mode 100644 index 36ab43864..000000000 --- a/docs/administration/CLI_tasks/release_environments.md +++ /dev/null @@ -1,9 +0,0 @@ -# Generate release environment file - -```sh tab="OTP" - ./bin/pleroma_ctl release_env gen -``` - -```sh tab="From Source" -mix pleroma.release_env gen -``` diff --git a/docs/administration/CLI_tasks/robots_txt.md b/docs/administration/CLI_tasks/robots_txt.md new file mode 100644 index 000000000..844318cc8 --- /dev/null +++ b/docs/administration/CLI_tasks/robots_txt.md @@ -0,0 +1,17 @@ +# Managing robots.txt + +{! backend/administration/CLI_tasks/general_cli_task_info.include !} + +## Generate a new robots.txt file and add it to the static directory + +The `robots.txt` that ships by default is permissive. It allows well-behaved search engines to index all of your instance's URIs. + +If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance [static directory](../../../configuration/static_dir/). + +```elixir tab="OTP" +./bin/pleroma_ctl robots_txt disallow_all +``` + +```elixir tab="From Source" +mix pleroma.robots_txt disallow_all +``` diff --git a/docs/clients.md b/docs/clients.md index ea751637e..2a42c659f 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -75,6 +75,13 @@ Feel free to contact us to be added to this list! - Platform: Android, iOS - Features: No Streaming +### Indigenous +- Homepage: +- Source Code: +- Contact: [@realize.be@realize.be](@realize.be@realize.be) +- Platforms: Android +- Features: No Streaming + ## Alternative Web Interfaces ### Brutaldon - Homepage: diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 9c768abef..e5742bc3a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -69,6 +69,10 @@ To add configuration to your config file, you can copy it from the base config. * `enabled`: Enables the send a direct message to a newly registered user. Defaults to `false`. * `sender_nickname`: The nickname of the local user that sends the welcome message. * `message`: A message that will be send to a newly registered users as a direct message. +* `chat_message`: - welcome message sent as a chat message. + * `enabled`: Enables the send a chat message to a newly registered user. Defaults to `false`. + * `sender_nickname`: The nickname of the local user that sends the welcome message. + * `message`: A message that will be send to a newly registered users as a chat message. * `email`: - welcome message sent as a email. * `enabled`: Enables the send a welcome email to a newly registered user. Defaults to `false`. * `sender`: The email address or tuple with `{nickname, email}` that will use as sender to the welcome email. @@ -110,6 +114,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.ActivityExpiration` to be enabled for processing the scheduled delections. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. @@ -125,6 +130,7 @@ To add configuration to your config file, you can copy it from the base config. * `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline. * `reject`: List of instances to reject any activities from. * `accept`: List of instances to accept any activities from. +* `followers_only`: List of instances to decrease post visibility to only the followers, including for DM mentions. * `report_removal`: List of instances to reject reports from. * `avatar_removal`: List of instances to strip avatars from. * `banner_removal`: List of instances to strip banners from. @@ -202,6 +208,11 @@ config :pleroma, :mrf_user_allowlist, %{ * `sign_object_fetches`: Sign object fetches with HTTP signatures * `authorized_fetch_mode`: Require HTTP signatures for AP fetches +## Pleroma.User + +* `restricted_nicknames`: List of nicknames users may not register with. +* `email_blacklist`: List of email domains users may not register with. + ## Pleroma.ScheduledActivity * `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`) @@ -210,6 +221,8 @@ config :pleroma, :mrf_user_allowlist, %{ ## Pleroma.ActivityExpiration +Enables the worker which processes posts scheduled for deletion. Pinned posts are exempt from expiration. + * `enabled`: whether expired activities will be sent to the job queue to be deleted ## Frontends @@ -848,9 +861,6 @@ Warning: it's discouraged to use this feature because of the associated security ### :auth -* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator. -* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication. - Authentication / authorization settings. * `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`. @@ -880,6 +890,9 @@ Pleroma account will be created with the same name as the LDAP user name. * `base`: LDAP base, e.g. "dc=example,dc=com" * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" +Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an +OpenLDAP server the value may be `uid: "uid"`. + ### OAuth consumer mode OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). diff --git a/docs/configuration/static_dir.md b/docs/configuration/static_dir.md index 5fb38c3de..58703e3be 100644 --- a/docs/configuration/static_dir.md +++ b/docs/configuration/static_dir.md @@ -1,45 +1,57 @@ # Static Directory -Static frontend files are shipped in `priv/static/` and tracked by version control in this repository. If you want to overwrite or update these without the possibility of merge conflicts, you can write your custom versions to `instance/static/`. +Static frontend files are shipped with pleroma. If you want to overwrite or update these without problems during upgrades, you can write your custom versions to the static directory. +You can find the location of the static directory in the [configuration](../cheatsheet/#instance). + +```elixir tab="OTP" +config :pleroma, :instance, + static_dir: "/var/lib/pleroma/static/", ``` + +```elixir tab="From Source" config :pleroma, :instance, static_dir: "instance/static/", ``` -For example, edit `instance/static/instance/panel.html` . - Alternatively, you can overwrite this value in your configuration to use a different static instance directory. -This document is written assuming `instance/static/`. +This document is written using `$static_dir` as the value of the `config :pleroma, :instance, static_dir` setting. -Or, if you want to manage your custom file in git repository, basically remove the `instance/` entry from `.gitignore`. +If you use a From Source installation and want to manage your custom files in the git repository, you can remove the `instance/` entry from `.gitignore`. ## robots.txt -By default, the `robots.txt` that ships in `priv/static/` is permissive. It allows well-behaved search engines to index all of your instance's URIs. +There's a mix tasks to [generate a new robot.txt](../../administration/CLI_tasks/robots_txt/). -If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance static directory. +For more complex things, you can write your own robots.txt to `$static_dir/robots.txt`. + +E.g. if you want to block all crawlers except for [fediverse.network](https://fediverse.network/about) you can use ``` -mix pleroma.robots_txt disallow_all +User-Agent: * +Disallow: / + +User-Agent: crawler-us-il-1.fediverse.network +Allow: / + +User-Agent: makhnovtchina.random.sh +Allow: / ``` ## Thumbnail -Put on `instance/static/instance/thumbnail.jpeg` with your selfie or other neat picture. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html). +Add `$static_dir/instance/thumbnail.jpeg` with your selfie or other neat picture. It will be available on `http://your-domain.tld/instance/thumbnail.jpeg` and can be used by external applications. ## Instance-specific panel -![instance-specific panel demo](/uploads/296b19ec806b130e0b49b16bfe29ce8a/image.png) - -Create and Edit your file on `instance/static/instance/panel.html`. +Create and Edit your file at `$static_dir/instance/panel.html`. ## Background -You can change the background of your Pleroma instance by uploading it to `instance/static/`, and then changing `background` in `config/prod.secret.exs` accordingly. +You can change the background of your Pleroma instance by uploading it to `$static_dir/`, and then changing `background` in [your configuration](../cheatsheet/#frontend_configurations) accordingly. -If you put `instance/static/images/background.jpg` +E.g. if you put `$static_dir/images/background.jpg` ``` config :pleroma, :frontend_configurations, @@ -50,12 +62,14 @@ config :pleroma, :frontend_configurations, ## Logo -![logo modification demo](/uploads/c70b14de60fa74245e7f0dcfa695ebff/image.png) +!!! important + Note the extra `static` folder for the default logo.png location -If you want to give a brand to your instance, You can change the logo of your instance by uploading it to `instance/static/`. +If you want to give a brand to your instance, You can change the logo of your instance by uploading it to the static directory `$static_dir/static/logo.png`. -Alternatively, you can specify the path with config. -If you put `instance/static/static/mylogo-file.png` +Alternatively, you can specify the path to your logo in [your configuration](../cheatsheet/#frontend_configurations). + +E.g. if you put `$static_dir/static/mylogo-file.png` ``` config :pleroma, :frontend_configurations, @@ -66,4 +80,7 @@ config :pleroma, :frontend_configurations, ## Terms of Service -Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by changing `instance/static/static/terms-of-service.html`. +!!! important + Note the extra `static` folder for the terms-of-service.html + +Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by adding and changing `$static_dir/static/terms-of-service.html`. diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index c726d559f..a5683f18c 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -14,6 +14,7 @@ It assumes that you have administrative rights, either as root or a user with [s * `erlang-xmerl` * `git` * Development Tools +* `cmake` #### Optional packages used in this guide @@ -39,7 +40,7 @@ sudo apk upgrade * Install some tools, which are needed later: ```shell -sudo apk add git build-base +sudo apk add git build-base cmake ``` ### Install Elixir and Erlang diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index bf9cfb488..7fb69dd60 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -9,6 +9,7 @@ This guide will assume that you have administrative rights, either as root or a * `elixir` * `git` * `base-devel` +* `cmake` #### Optional packages used in this guide @@ -26,7 +27,7 @@ sudo pacman -Syu * Install some of the above mentioned programs: ```shell -sudo pacman -S git base-devel elixir +sudo pacman -S git base-devel elixir cmake ``` ### Install PostgreSQL diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 8ae5044b5..60c2f47e5 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -12,6 +12,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi * `erlang-nox` * `git` * `build-essential` +* `cmake` #### Optional packages used in this guide @@ -30,7 +31,7 @@ sudo apt full-upgrade * Install some of the above mentioned programs: ```shell -sudo apt install git build-essential postgresql postgresql-contrib +sudo apt install git build-essential postgresql postgresql-contrib cmake ``` ### Install Elixir and Erlang diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 42e91cda7..c2dd840d3 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -16,6 +16,7 @@ - `erlang-nox` - `git` - `build-essential` +- `cmake` #### このガイドで利用している追加パッケージ @@ -32,7 +33,7 @@ sudo apt full-upgrade * 上記に挙げたパッケージをインストールしておきます。 ``` -sudo apt install git build-essential postgresql postgresql-contrib +sudo apt install git build-essential postgresql postgresql-contrib cmake ``` diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 32152aea7..5a676380c 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -28,6 +28,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * `dev-db/postgresql` * `dev-lang/elixir` * `dev-vcs/git` +* `dev-util/cmake` #### Optional ebuilds used in this guide @@ -46,7 +47,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * Emerge all required the required and suggested software in one go: ```shell - # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx + # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake ``` If you would not like to install the optional packages, remove them from this line. diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index 3626acc69..6ad0de2f6 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -19,6 +19,7 @@ databases/postgresql11-client databases/postgresql11-server devel/git-base devel/git-docs +devel/cmake lang/elixir security/acmesh security/sudo diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 5dbe24f75..eee452845 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -14,11 +14,12 @@ The following packages need to be installed: * git * postgresql-server * postgresql-contrib + * cmake To install them, run the following command (with doas or as root): ``` -pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib +pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib cmake ``` Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index 272273cff..b5b5056a9 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -16,7 +16,7 @@ Matrix-kanava #freenode_#pleroma:matrix.org ovat hyviä paikkoja löytää apua Asenna tarvittava ohjelmisto: -`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3` +`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake` Luo postgresql-tietokanta: diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 338dfa7d0..e4f822d1c 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -121,9 +121,6 @@ chown -R pleroma /etc/pleroma # Run the config generator su pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" -# Run the environment file generator. -su pleroma -s $SHELL -lc "./bin/pleroma_ctl release_env gen" - # Create the postgres database su postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" @@ -134,7 +131,7 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate" # su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" # Start the instance to verify that everything is working as expected -su pleroma -s $SHELL -lc "export $(cat /opt/pleroma/config/pleroma.env); ./bin/pleroma daemon" +su pleroma -s $SHELL -lc "./bin/pleroma daemon" # Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly sleep 20 && curl http://localhost:4000/api/v1/instance @@ -203,7 +200,6 @@ rc-update add pleroma # Copy the service into a proper directory cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service - # Start pleroma and enable it on boot systemctl start pleroma systemctl enable pleroma @@ -279,3 +275,4 @@ This will create an account withe the username of 'joeuser' with the email addre ## Questions Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. + diff --git a/installation/init.d/pleroma b/installation/init.d/pleroma index e908cda1b..384536f7e 100755 --- a/installation/init.d/pleroma +++ b/installation/init.d/pleroma @@ -8,7 +8,6 @@ pidfile="/var/run/pleroma.pid" directory=/opt/pleroma healthcheck_delay=60 healthcheck_timer=30 -export $(cat /opt/pleroma/config/pleroma.env) : ${pleroma_port:-4000} diff --git a/installation/pleroma.service b/installation/pleroma.service index ee00a3b7a..5dcbc1387 100644 --- a/installation/pleroma.service +++ b/installation/pleroma.service @@ -17,8 +17,6 @@ Environment="MIX_ENV=prod" Environment="HOME=/var/lib/pleroma" ; Path to the folder containing the Pleroma installation. WorkingDirectory=/opt/pleroma -; Path to the environment file. the file contains RELEASE_COOKIE and etc -EnvironmentFile=/opt/pleroma/config/pleroma.env ; Path to the Mix binary. ExecStart=/usr/bin/mix phx.server diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 82e2abdcb..7d8f00b08 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -10,6 +10,7 @@ defmodule Mix.Tasks.Pleroma.Database do alias Pleroma.User require Logger require Pleroma.Constants + import Ecto.Query import Mix.Pleroma use Mix.Task @@ -53,8 +54,6 @@ def run(["update_users_following_followers_counts"]) do end def run(["prune_objects" | args]) do - import Ecto.Query - {options, [], []} = OptionParser.parse( args, @@ -94,8 +93,6 @@ def run(["prune_objects" | args]) do end def run(["fix_likes_collections"]) do - import Ecto.Query - start_pleroma() from(object in Object, @@ -130,4 +127,33 @@ def run(["vacuum", args]) do Maintenance.vacuum(args) end + + def run(["ensure_expiration"]) do + start_pleroma() + days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + + Pleroma.Activity + |> join(:left, [a], u in assoc(a, :expiration)) + |> join(:inner, [a, _u], o in Object, + on: + fragment( + "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", + o.data, + a.data, + a.data + ) + ) + |> where(local: true) + |> where([a, u], is_nil(u)) + |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) + |> where([_a, _u, o], fragment("?->>'type' = 'Note'", o.data)) + |> Pleroma.RepoStreamer.chunk_stream(100) + |> Stream.each(fn activities -> + Enum.each(activities, fn activity -> + expires_at = Timex.shift(activity.inserted_at, days: days) + Pleroma.ActivityExpiration.create(activity, expires_at, false) + end) + end) + |> Stream.run() + end end diff --git a/lib/mix/tasks/pleroma/release_env.ex b/lib/mix/tasks/pleroma/release_env.ex deleted file mode 100644 index 9da74ffcf..000000000 --- a/lib/mix/tasks/pleroma/release_env.ex +++ /dev/null @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.ReleaseEnv do - use Mix.Task - import Mix.Pleroma - - @shortdoc "Generate Pleroma environment file." - @moduledoc File.read!("docs/administration/CLI_tasks/release_environments.md") - - def run(["gen" | rest]) do - {options, [], []} = - OptionParser.parse( - rest, - strict: [ - force: :boolean, - path: :string - ], - aliases: [ - p: :path, - f: :force - ] - ) - - file_path = - get_option( - options, - :path, - "Environment file path", - "./config/pleroma.env" - ) - - env_path = Path.expand(file_path) - - proceed? = - if File.exists?(env_path) do - get_option( - options, - :force, - "Environment file already exists. Do you want to overwrite the #{env_path} file? (y/n)", - "n" - ) === "y" - else - true - end - - if proceed? do - case do_generate(env_path) do - {:error, reason} -> - shell_error( - File.Error.message(%{action: "write to file", reason: reason, path: env_path}) - ) - - _ -> - shell_info("\nThe file generated: #{env_path}.\n") - - shell_info(""" - WARNING: before start pleroma app please make sure to make the file read-only and non-modifiable. - Example: - chmod 0444 #{file_path} - chattr +i #{file_path} - """) - end - else - shell_info("\nThe file is exist. #{env_path}.\n") - end - end - - def do_generate(path) do - content = "RELEASE_COOKIE=#{Base.encode32(:crypto.strong_rand_bytes(32))}" - - File.mkdir_p!(Path.dirname(path)) - File.write(path, content) - end -end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index c3cea8d2a..97feebeaa 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -340,4 +340,10 @@ def direct_conversation_id(activity, for_user) do _ -> nil end end + + @spec pinned_by_actor?(Activity.t()) :: boolean() + def pinned_by_actor?(%Activity{} = activity) do + actor = user_actor(activity) + activity.id in actor.pinned_activities + end end diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex index db9c88d84..955f0578e 100644 --- a/lib/pleroma/activity_expiration.ex +++ b/lib/pleroma/activity_expiration.ex @@ -20,11 +20,11 @@ defmodule Pleroma.ActivityExpiration do field(:scheduled_at, :naive_datetime) end - def changeset(%ActivityExpiration{} = expiration, attrs) do + def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do expiration |> cast(attrs, [:scheduled_at]) |> validate_required([:scheduled_at]) - |> validate_scheduled_at() + |> validate_scheduled_at(validate_scheduled_at) end def get_by_activity_id(activity_id) do @@ -33,9 +33,9 @@ def get_by_activity_id(activity_id) do |> Repo.one() end - def create(%Activity{} = activity, scheduled_at) do + def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do %ActivityExpiration{activity_id: activity.id} - |> changeset(%{scheduled_at: scheduled_at}) + |> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at) |> Repo.insert() end @@ -46,10 +46,17 @@ def due_expirations(offset \\ 0) do ActivityExpiration |> where([exp], exp.scheduled_at < ^naive_datetime) + |> limit(50) + |> preload(:activity) |> Repo.all() + |> Enum.reject(fn %{activity: activity} -> + Activity.pinned_by_actor?(activity) + end) end - def validate_scheduled_at(changeset) do + def validate_scheduled_at(changeset, false), do: changeset + + def validate_scheduled_at(changeset, true) do validate_change(changeset, :scheduled_at, fn _, scheduled_at -> if not expires_late_enough?(scheduled_at) do [scheduled_at: "an ephemeral activity must live for at least one hour"] diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 0ffb55358..c0b5db9f1 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -47,6 +47,7 @@ def start(_type, _args) do Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() + check_system_commands() Pleroma.Docs.JSON.compile() adapter = Application.get_env(:tesla, :adapter) @@ -249,4 +250,21 @@ defp http_children(Tesla.Adapter.Gun, _) do end defp http_children(_, _), do: [] + + defp check_system_commands do + filters = Config.get([Pleroma.Upload, :filters]) + + check_filter = fn filter, command_required -> + with true <- filter in filters, + false <- Pleroma.Utils.command_available?(command_required) do + Logger.error( + "#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found" + ) + end + end + + check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool") + check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify") + check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify") + end end diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index cc80deff5..a8329cc1e 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -11,12 +11,10 @@ def get(key), do: get(key, nil) def get([key], default), do: get(key, default) - def get([parent_key | keys], default) do - case :pleroma - |> Application.get_env(parent_key) - |> get_in(keys) do - nil -> default - any -> any + def get([_ | _] = path, default) do + case fetch(path) do + {:ok, value} -> value + :error -> default end end @@ -34,6 +32,24 @@ def get!(key) do end end + def fetch(key) when is_atom(key), do: fetch([key]) + + def fetch([root_key | keys]) do + Enum.reduce_while(keys, Application.fetch_env(:pleroma, root_key), fn + key, {:ok, config} when is_map(config) or is_list(config) -> + case Access.fetch(config, key) do + :error -> + {:halt, :error} + + value -> + {:cont, value} + end + + _key, _config -> + {:halt, :error} + end) + end + def put([key], value), do: put(key, value) def put([parent_key | keys], value) do @@ -50,12 +66,15 @@ def put(key, value) do def delete([key]), do: delete(key) - def delete([parent_key | keys]) do - {_, parent} = - Application.get_env(:pleroma, parent_key) - |> get_and_update_in(keys, fn _ -> :pop end) + def delete([parent_key | keys] = path) do + with {:ok, _} <- fetch(path) do + {_, parent} = + parent_key + |> get() + |> get_and_update_in(keys, fn _ -> :pop end) - Application.put_env(:pleroma, parent_key, parent) + Application.put_env(:pleroma, parent_key, parent) + end end def delete(key) do diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index c2020d30a..83b366dd4 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -95,7 +95,11 @@ def followers_query(%User{} = user) do |> where([r], r.state == ^:follow_accept) end - def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do + def followers_ap_ids(user, from_ap_ids \\ nil) + + def followers_ap_ids(_, []), do: [] + + def followers_ap_ids(%User{} = user, from_ap_ids) do query = user |> followers_query() diff --git a/lib/pleroma/job_queue_monitor.ex b/lib/pleroma/job_queue_monitor.ex index 2ecf261f3..c255a61ec 100644 --- a/lib/pleroma/job_queue_monitor.ex +++ b/lib/pleroma/job_queue_monitor.ex @@ -15,8 +15,8 @@ def start_link(_) do @impl true def init(state) do - :telemetry.attach("oban-monitor-failure", [:oban, :failure], &handle_event/4, nil) - :telemetry.attach("oban-monitor-success", [:oban, :success], &handle_event/4, nil) + :telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil) + :telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil) {:ok, state} end @@ -25,8 +25,11 @@ def stats do GenServer.call(__MODULE__, :stats) end - def handle_event([:oban, status], %{duration: duration}, meta, _) do - GenServer.cast(__MODULE__, {:process_event, status, duration, meta}) + def handle_event([:oban, :job, event], %{duration: duration}, meta, _) do + GenServer.cast( + __MODULE__, + {:process_event, mapping_status(event), duration, meta} + ) end @impl true @@ -75,4 +78,7 @@ defp update_queue(queue, status, _meta, _duration) do |> Map.update!(:processed_jobs, &(&1 + 1)) |> Map.update!(status, &(&1 + 1)) end + + defp mapping_status(:stop), do: :success + defp mapping_status(:exception), do: :failure end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 546c4ea01..052ad413b 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -255,6 +255,10 @@ def increase_replies_count(ap_id) do end end + defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true + + defp poll_is_multiple?(_), do: false + def decrease_replies_count(ap_id) do Object |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id))) @@ -281,10 +285,10 @@ def decrease_replies_count(ap_id) do def increase_vote_count(ap_id, name, actor) do with %Object{} = object <- Object.normalize(ap_id), "Question" <- object.data["type"] do - multiple = Map.has_key?(object.data, "anyOf") + key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf" options = - (object.data["anyOf"] || object.data["oneOf"] || []) + object.data[key] |> Enum.map(fn %{"name" => ^name} = option -> Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1)) @@ -296,11 +300,8 @@ def increase_vote_count(ap_id, name, actor) do voters = [actor | object.data["voters"] || []] |> Enum.uniq() data = - if multiple do - Map.put(object.data, "anyOf", options) - else - Map.put(object.data, "oneOf", options) - end + object.data + |> Map.put(key, options) |> Map.put("voters", voters) object diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 99608b8a5..bc88e8a0c 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -55,7 +55,7 @@ defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do defp compare_uris(_id_uri, _other_uri), do: :error @doc """ - Checks that an imported AP object's actor matches the domain it came from. + Checks that an imported AP object's actor matches the host it came from. """ def contain_origin(_id, %{"actor" => nil}), do: :error diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index e74c87269..3ff25118d 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.Repo alias Pleroma.Signature alias Pleroma.Web.ActivityPub.InternalFetchActor + alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.Federator @@ -23,21 +24,39 @@ defp touch_changeset(changeset) do Ecto.Changeset.put_change(changeset, :updated_at, updated_at) end - defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do + defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields()) - Map.merge(data, internal_fields) + Map.merge(new_data, internal_fields) end - defp maybe_reinject_internal_fields(data, _), do: data + defp maybe_reinject_internal_fields(_, new_data), do: new_data @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()} - defp reinject_object(struct, data) do - Logger.debug("Reinjecting object #{data["id"]}") + defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do + Logger.debug("Reinjecting object #{new_data["id"]}") - with data <- Transmogrifier.fix_object(data), - data <- maybe_reinject_internal_fields(data, struct), - changeset <- Object.change(struct, %{data: data}), + with new_data <- Transmogrifier.fix_object(new_data), + data <- maybe_reinject_internal_fields(object, new_data), + {:ok, data, _} <- ObjectValidator.validate(data, %{}), + changeset <- Object.change(object, %{data: data}), + changeset <- touch_changeset(changeset), + {:ok, object} <- Repo.insert_or_update(changeset), + {:ok, object} <- Object.set_cache(object) do + {:ok, object} + else + e -> + Logger.error("Error while processing object: #{inspect(e)}") + {:error, e} + end + end + + defp reinject_object(%Object{} = object, new_data) do + Logger.debug("Reinjecting object #{new_data["id"]}") + + with new_data <- Transmogrifier.fix_object(new_data), + data <- maybe_reinject_internal_fields(object, new_data), + changeset <- Object.change(object, %{data: data}), changeset <- touch_changeset(changeset), {:ok, object} <- Repo.insert_or_update(changeset), {:ok, object} <- Object.set_cache(object) do @@ -51,8 +70,8 @@ defp reinject_object(struct, data) do def refetch_object(%Object{data: %{"id" => id}} = object) do with {:local, false} <- {:local, Object.local?(object)}, - {:ok, data} <- fetch_and_contain_remote_object_from_id(id), - {:ok, object} <- reinject_object(object, data) do + {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id), + {:ok, object} <- reinject_object(object, new_data) do {:ok, object} else {:local, true} -> {:ok, object} diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex index c7fb6aefa..ea8798fe3 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -9,9 +9,17 @@ defmodule Pleroma.Upload.Filter.Exiftool do """ @behaviour Pleroma.Upload.Filter + @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do - System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) - :ok + try do + case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do + {_response, 0} -> :ok + {error, 1} -> {:error, error} + end + rescue + _e in ErlangError -> + {:error, "exiftool command not found"} + end end def filter(_), do: :ok diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex index 7d95577a4..a8503ac24 100644 --- a/lib/pleroma/upload/filter/mogrifun.ex +++ b/lib/pleroma/upload/filter/mogrifun.ex @@ -34,10 +34,15 @@ defmodule Pleroma.Upload.Filter.Mogrifun do [{"fill", "yellow"}, {"tint", "40"}] ] + @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do - Filter.Mogrify.do_filter(file, [Enum.random(@filters)]) - - :ok + try do + Filter.Mogrify.do_filter(file, [Enum.random(@filters)]) + :ok + rescue + _e in ErlangError -> + {:error, "mogrify command not found"} + end end def filter(_), do: :ok diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex index 2eb758006..7a45add5a 100644 --- a/lib/pleroma/upload/filter/mogrify.ex +++ b/lib/pleroma/upload/filter/mogrify.ex @@ -8,11 +8,15 @@ defmodule Pleroma.Upload.Filter.Mogrify do @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] + @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do - filters = Pleroma.Config.get!([__MODULE__, :args]) - - do_filter(file, filters) - :ok + try do + do_filter(file, Pleroma.Config.get!([__MODULE__, :args])) + :ok + rescue + _e in ErlangError -> + {:error, "mogrify command not found"} + end end def filter(_), do: :ok diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index dcf6ebee2..d1436a688 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -638,6 +638,34 @@ def force_password_reset_async(user) do @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def force_password_reset(user), do: update_password_reset_pending(user, true) + # Used to auto-register LDAP accounts which won't have a password hash stored locally + def register_changeset_ldap(struct, params = %{password: password}) + when is_nil(password) do + params = Map.put_new(params, :accepts_chat_messages, true) + + params = + if Map.has_key?(params, :email) do + Map.put_new(params, :email, params[:email]) + else + params + end + + struct + |> cast(params, [ + :name, + :nickname, + :email, + :accepts_chat_messages + ]) + |> validate_required([:name, :nickname]) + |> unique_constraint(:nickname) + |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames])) + |> validate_format(:nickname, local_nickname_regex()) + |> put_ap_id() + |> unique_constraint(:ap_id) + |> put_following_and_follower_address() + end + def register_changeset(struct, params \\ %{}, opts \\ []) do bio_limit = Config.get([:instance, :user_bio_length], 5000) name_limit = Config.get([:instance, :user_name_length], 100) @@ -676,10 +704,19 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) |> unique_constraint(:email) + |> validate_format(:email, @email_regex) + |> validate_change(:email, fn :email, email -> + valid? = + Config.get([User, :email_blacklist]) + |> Enum.all?(fn blacklisted_domain -> + !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain]) + end) + + if valid?, do: [], else: [email: "Invalid email"] + end) |> unique_constraint(:nickname) |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) - |> validate_format(:email, @email_regex) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) |> validate_length(:registration_reason, max: reason_limit) @@ -734,6 +771,7 @@ def post_register_action(%User{} = user) do {:ok, user} <- set_cache(user), {:ok, _} <- send_welcome_email(user), {:ok, _} <- send_welcome_message(user), + {:ok, _} <- send_welcome_chat_message(user), {:ok, _} <- try_send_confirmation_email(user) do {:ok, user} end @@ -748,6 +786,15 @@ def send_welcome_message(user) do end end + def send_welcome_chat_message(user) do + if User.WelcomeChatMessage.enabled?() do + User.WelcomeChatMessage.post_message(user) + {:ok, :enqueued} + else + {:ok, :noop} + end + end + def send_welcome_email(%User{email: email} = user) when is_binary(email) do if User.WelcomeEmail.enabled?() do User.WelcomeEmail.send_email(user) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 45553cb6c..d618432ff 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -130,6 +130,7 @@ defp compose_query({:external, _}, query), do: location_query(query, false) defp compose_query({:active, _}, query) do User.restrict_deactivated(query) |> where([u], not is_nil(u.nickname)) + |> where([u], u.approval_pending == false) end defp compose_query({:legacy_active, _}, query) do diff --git a/lib/pleroma/user/welcome_chat_message.ex b/lib/pleroma/user/welcome_chat_message.ex new file mode 100644 index 000000000..3e7d1f424 --- /dev/null +++ b/lib/pleroma/user/welcome_chat_message.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeChatMessage do + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + @spec enabled?() :: boolean() + def enabled?, do: Config.get([:welcome, :chat_message, :enabled], false) + + @spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil} + def post_message(user) do + [:welcome, :chat_message, :sender_nickname] + |> Config.get(nil) + |> fetch_sender() + |> do_post(user, welcome_message()) + end + + defp do_post(%User{} = sender, recipient, message) + when is_binary(message) do + CommonAPI.post_chat_message( + sender, + recipient, + message + ) + end + + defp do_post(_sender, _recipient, _message), do: {:ok, nil} + + defp fetch_sender(nickname) when is_binary(nickname) do + with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do + user + else + _ -> nil + end + end + + defp fetch_sender(_), do: nil + + defp welcome_message do + Config.get([:welcome, :chat_message, :message], nil) + end +end diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 6b8e3accf..21d1159be 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -9,4 +9,19 @@ def compile_dir(dir) when is_binary(dir) do |> Enum.map(&Path.join(dir, &1)) |> Kernel.ParallelCompiler.compile() end + + @doc """ + POSIX-compliant check if command is available in the system + + ## Examples + iex> command_available?("git") + true + iex> command_available?("wrongcmd") + false + + """ + @spec command_available?(String.t()) :: boolean() + def command_available?(command) do + match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"])) + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a4db1d87c..bde1fe708 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -66,7 +66,7 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil( defp check_remote_limit(_), do: true - defp increase_note_count_if_public(actor, object) do + def increase_note_count_if_public(actor, object) do if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor} end @@ -85,17 +85,7 @@ defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(_create_data), do: :noop - defp increase_poll_votes_if_vote(%{ - "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, - "type" => "Create", - "actor" => actor - }) do - Object.increase_vote_count(reply_ap_id, name, actor) - end - - defp increase_poll_votes_if_vote(_create_data), do: :noop - - @object_types ["ChatMessage"] + @object_types ["ChatMessage", "Question", "Answer"] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do @@ -258,7 +248,6 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param with {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), - _ <- increase_poll_votes_if_vote(create_data), {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity}, {:ok, _actor} <- increase_note_count_if_public(actor, activity), _ <- notify_and_stream(activity), @@ -296,32 +285,6 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d end end - @spec accept(map()) :: {:ok, Activity.t()} | {:error, any()} - def accept(params) do - accept_or_reject("Accept", params) - end - - @spec reject(map()) :: {:ok, Activity.t()} | {:error, any()} - def reject(params) do - accept_or_reject("Reject", params) - end - - @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} - defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do - local = Map.get(params, :local, true) - activity_id = Map.get(params, :activity_id, nil) - - data = - %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} - |> Maps.put_if_present("id", activity_id) - - with {:ok, activity} <- insert(data, local), - _ <- notify_and_stream(activity), - :ok <- maybe_federate(activity) do - {:ok, activity} - end - end - @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) :: {:ok, Activity.t()} | nil | {:error, any()} def unfollow(follower, followed, activity_id \\ nil, local \\ true) do diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index d5f3610ed..f2392ce79 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -14,6 +14,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do require Pleroma.Constants + def accept_or_reject(actor, activity, type) do + data = %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "type" => type, + "object" => activity.data["id"], + "to" => [activity.actor] + } + + {:ok, data, []} + end + + @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()} + def reject(actor, rejected_activity) do + accept_or_reject(actor, rejected_activity, "Reject") + end + + @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()} + def accept(actor, accepted_activity) do + accept_or_reject(actor, accepted_activity, "Accept") + end + @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} def follow(follower, followed) do data = %{ @@ -80,6 +102,13 @@ def delete(actor, object_id) do end def create(actor, object, recipients) do + context = + if is_map(object) do + object["context"] + else + nil + end + {:ok, %{ "id" => Utils.generate_activity_id(), @@ -88,7 +117,8 @@ def create(actor, object, recipients) do "object" => object, "type" => "Create", "published" => DateTime.utc_now() |> DateTime.to_iso8601() - }, []} + } + |> Pleroma.Maps.put_if_present("context", context), []} end def chat_message(actor, recipient, content, opts \\ []) do @@ -115,6 +145,22 @@ def chat_message(actor, recipient, content, opts \\ []) do end end + def answer(user, object, name) do + {:ok, + %{ + "type" => "Answer", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "cc" => [object.data["actor"]], + "to" => [], + "name" => name, + "inReplyTo" => object.data["id"], + "context" => object.data["context"], + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "id" => Utils.generate_object_id() + }, []} + end + @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()} def tombstone(actor, id) do {:ok, diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 8e47f1e02..7b4c78e0f 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -21,8 +21,8 @@ def filter(activity) do @impl true def describe, do: {:ok, %{}} - defp local?(%{"id" => id}) do - String.starts_with?(id, Pleroma.Web.Endpoint.url()) + defp local?(%{"actor" => actor}) do + String.starts_with?(actor, Pleroma.Web.Endpoint.url()) end defp note?(activity) do diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index 5f111c72f..d45d2d7e3 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -37,8 +37,13 @@ defp check_reject(message, actions) do defp check_delist(message, actions) do if :delist in actions do with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do - to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address] - cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()] + to = + List.delete(message["to"] || [], Pleroma.Constants.as_public()) ++ + [user.follower_address] + + cc = + List.delete(message["cc"] || [], user.follower_address) ++ + [Pleroma.Constants.as_public()] message = message @@ -58,8 +63,8 @@ defp check_delist(message, actions) do defp check_strip_followers(message, actions) do if :strip_followers in actions do with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do - to = List.delete(message["to"], user.follower_address) - cc = List.delete(message["cc"], user.follower_address) + to = List.delete(message["to"] || [], user.follower_address) + cc = List.delete(message["cc"] || [], user.follower_address) message = message diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index b77b8c7b4..bb193475a 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do @behaviour Pleroma.Web.ActivityPub.MRF alias Pleroma.Config + alias Pleroma.FollowingRelationship alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF @@ -108,6 +109,35 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do {:ok, object} end + defp intersection(list1, list2) do + list1 -- list1 -- list2 + end + + defp check_followers_only(%{host: actor_host} = _actor_info, object) do + followers_only = + Config.get([:mrf_simple, :followers_only]) + |> MRF.subdomains_regex() + + object = + with true <- MRF.subdomain_match?(followers_only, actor_host), + user <- User.get_cached_by_ap_id(object["actor"]) do + # Don't use Map.get/3 intentionally, these must not be nil + fixed_to = object["to"] || [] + fixed_cc = object["cc"] || [] + + to = FollowingRelationship.followers_ap_ids(user, fixed_to) + cc = FollowingRelationship.followers_ap_ids(user, fixed_cc) + + object + |> Map.put("to", intersection([user.follower_address | to], fixed_to)) + |> Map.put("cc", intersection([user.follower_address | cc], fixed_cc)) + else + _ -> object + end + + {:ok, object} + end + defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do report_removal = Config.get([:mrf_simple, :report_removal]) @@ -174,6 +204,7 @@ def filter(%{"actor" => actor} = object) do {: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) do {:ok, object} else diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 0dcc7be4d..3f1dffe2b 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -13,20 +13,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) + def validate(%{"type" => type} = object, meta) + when type in ~w[Accept Reject] do + with {:ok, object} <- + object + |> AcceptRejectValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => "Follow"} = object, meta) do with {:ok, object} <- object @@ -112,17 +127,40 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do end end + def validate(%{"type" => "Question"} = object, meta) do + with {:ok, object} <- + object + |> QuestionValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Answer"} = object, meta) do + with {:ok, object} <- + object + |> AnswerValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => "EmojiReact"} = object, meta) do with {:ok, object} <- object |> EmojiReactValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object |> Map.from_struct()) + object = stringify_keys(object) {:ok, object, meta} end end - def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do + def validate( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity, + meta + ) do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -134,12 +172,28 @@ def validate(%{"type" => "Create", "object" => object} = create_activity, meta) end end + def validate( + %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, + meta + ) + when objtype in ["Question", "Answer"] do + with {:ok, object_data} <- cast_and_apply(object), + meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), + {:ok, create_activity} <- + create_activity + |> CreateGenericValidator.cast_and_validate(meta) + |> Ecto.Changeset.apply_action(:insert) do + create_activity = stringify_keys(create_activity) + {:ok, create_activity, meta} + end + end + def validate(%{"type" => "Announce"} = object, meta) do with {:ok, object} <- object |> AnnounceValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object |> Map.from_struct()) + object = stringify_keys(object) {:ok, object, meta} end end @@ -148,8 +202,17 @@ def cast_and_apply(%{"type" => "ChatMessage"} = object) do ChatMessageValidator.cast_and_apply(object) end + def cast_and_apply(%{"type" => "Question"} = object) do + QuestionValidator.cast_and_apply(object) + end + + def cast_and_apply(%{"type" => "Answer"} = object) do + AnswerValidator.cast_and_apply(object) + end + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} + # is_struct/1 isn't present in Elixir 1.8.x def stringify_keys(%{__struct__: _} = object) do object |> Map.from_struct() diff --git a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex new file mode 100644 index 000000000..179beda58 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + end + + def cast_data(data) do + %__MODULE__{} + |> cast(data, __schema__(:fields)) + end + + def validate_data(cng) do + cng + |> validate_required([:id, :type, :actor, :to, :cc, :object]) + |> validate_inclusion(:type, ["Accept", "Reject"]) + |> validate_actor_presence() + |> validate_object_presence(allowed_types: ["Follow"]) + |> validate_accept_reject_rights() + end + + def cast_and_validate(data) do + data + |> cast_data + |> validate_data + end + + def validate_accept_reject_rights(cng) do + with object_id when is_binary(object_id) <- get_field(cng, :object), + %Activity{data: %{"object" => followed_actor}} <- Activity.get_by_ap_id(object_id), + true <- followed_actor == get_field(cng, :actor) do + cng + else + _e -> + cng + |> add_error(:actor, "can't accept or reject the given activity") + end + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex new file mode 100644 index 000000000..323367642 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:to, {:array, :string}, default: []) + field(:cc, {:array, :string}, default: []) + + # is this actually needed? + field(:bto, {:array, :string}, default: []) + field(:bcc, {:array, :string}, default: []) + + field(:type, :string) + field(:name, :string) + field(:inReplyTo, :string) + field(:attributedTo, ObjectValidators.ObjectID) + + # TODO: Remove actor on objects + field(:actor, ObjectValidators.ObjectID) + end + + def cast_and_apply(data) do + data + |> cast_data() + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def changeset(struct, data) do + struct + |> cast(data, __schema__(:fields)) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["Answer"]) + |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor]) + |> CommonValidations.validate_any_presence([:cc, :to]) + |> CommonValidations.validate_fields_match([:actor, :attributedTo]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_host_match() + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index aeef31945..603d87b8e 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do alias Pleroma.Object alias Pleroma.User - def validate_recipients_presence(cng, fields \\ [:to, :cc]) do + def validate_any_presence(cng, fields) do non_empty = fields |> Enum.map(fn field -> get_field(cng, field) end) @@ -24,7 +24,7 @@ def validate_recipients_presence(cng, fields \\ [:to, :cc]) do fields |> Enum.reduce(cng, fn field, cng -> cng - |> add_error(field, "no recipients in any field") + |> add_error(field, "none of #{inspect(fields)} present") end) end end @@ -34,10 +34,15 @@ def validate_actor_presence(cng, options \\ []) do cng |> validate_change(field_name, fn field_name, actor -> - if User.get_cached_by_ap_id(actor) do - [] - else - [{field_name, "can't find user"}] + case User.get_cached_by_ap_id(actor) do + %User{deactivated: true} -> + [{field_name, "user is deactivated"}] + + %User{} -> + [] + + _ -> + [{field_name, "can't find user"}] end end) end @@ -77,4 +82,60 @@ def validate_object_or_user_presence(cng, options \\ []) do if actor_cng.valid?, do: actor_cng, else: object_cng end + + def validate_host_match(cng, fields \\ [:id, :actor]) do + if same_domain?(cng, fields) do + cng + else + fields + |> Enum.reduce(cng, fn field, cng -> + cng + |> add_error(field, "hosts of #{inspect(fields)} aren't matching") + end) + end + end + + def validate_fields_match(cng, fields) do + if map_unique?(cng, fields) do + cng + else + fields + |> Enum.reduce(cng, fn field, cng -> + cng + |> add_error(field, "Fields #{inspect(fields)} aren't matching") + end) + end + end + + defp map_unique?(cng, fields, func \\ & &1) do + Enum.reduce_while(fields, nil, fn field, acc -> + value = + cng + |> get_field(field) + |> func.() + + case {value, acc} do + {value, nil} -> {:cont, value} + {value, value} -> {:cont, value} + _ -> {:halt, false} + end + end) + end + + def same_domain?(cng, fields \\ [:actor, :object]) do + map_unique?(cng, fields, fn value -> URI.parse(value).host end) + end + + # This figures out if a user is able to create, delete or modify something + # based on the domain and superuser status + def validate_modification_rights(cng) do + actor = User.get_cached_by_ap_id(get_field(cng, :actor)) + + if User.superuser?(actor) || same_domain?(cng) do + cng + else + cng + |> add_error(:actor, "is not allowed to modify object") + end + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex new file mode 100644 index 000000000..60868eae0 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -0,0 +1,133 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +# Code based on CreateChatMessageValidator +# NOTES +# - doesn't embed, will only get the object id +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Object + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:actor, ObjectValidators.ObjectID) + field(:type, :string) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:object, ObjectValidators.ObjectID) + field(:expires_at, ObjectValidators.DateTime) + + # Should be moved to object, done for CommonAPI.Utils.make_context + field(:context, :string) + end + + def cast_data(data, meta \\ []) do + data = fix(data, meta) + + %__MODULE__{} + |> changeset(data) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data, meta \\ []) do + data + |> cast_data(meta) + |> validate_data(meta) + end + + def changeset(struct, data) do + struct + |> cast(data, __schema__(:fields)) + end + + defp fix_context(data, meta) do + if object = meta[:object_data] do + Map.put_new(data, "context", object["context"]) + else + data + end + end + + defp fix(data, meta) do + data + |> fix_context(meta) + end + + def validate_data(cng, meta \\ []) do + cng + |> validate_required([:actor, :type, :object]) + |> validate_inclusion(:type, ["Create"]) + |> validate_actor_presence() + |> validate_any_presence([:to, :cc]) + |> validate_actors_match(meta) + |> validate_context_match(meta) + |> validate_object_nonexistence() + |> validate_object_containment() + end + + def validate_object_containment(cng) do + actor = get_field(cng, :actor) + + cng + |> validate_change(:object, fn :object, object_id -> + %URI{host: object_id_host} = URI.parse(object_id) + %URI{host: actor_host} = URI.parse(actor) + + if object_id_host == actor_host do + [] + else + [{:object, "The host of the object id doesn't match with the host of the actor"}] + end + end) + end + + def validate_object_nonexistence(cng) do + cng + |> validate_change(:object, fn :object, object_id -> + if Object.get_cached_by_ap_id(object_id) do + [{:object, "The object to create already exists"}] + else + [] + end + end) + end + + def validate_actors_match(cng, meta) do + attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"] + + cng + |> validate_change(:actor, fn :actor, actor -> + if actor == attributed_to do + [] + else + [{:actor, "Actor doesn't match with object attributedTo"}] + end + end) + end + + def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do + cng + |> validate_change(:context, fn :context, context -> + if context == object_context do + [] + else + [{:context, "context field not matching between Create and object (#{object_context})"}] + end + end) + end + + def validate_context_match(cng, _), do: cng +end diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index 93a7b0e0b..2634e8d4d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -59,7 +58,7 @@ def validate_data(cng) do |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Delete"]) |> validate_actor_presence() - |> validate_deletion_rights() + |> validate_modification_rights() |> validate_object_or_user_presence(allowed_types: @deletable_types) |> add_deleted_activity_id() end @@ -68,31 +67,6 @@ def do_not_federate?(cng) do !same_domain?(cng) end - defp same_domain?(cng) do - actor_uri = - cng - |> get_field(:actor) - |> URI.parse() - - object_uri = - cng - |> get_field(:object) - |> URI.parse() - - object_uri.host == actor_uri.host - end - - def validate_deletion_rights(cng) do - actor = User.get_cached_by_ap_id(get_field(cng, :actor)) - - if User.superuser?(actor) || same_domain?(cng) do - cng - else - cng - |> add_error(:actor, "is not allowed to delete object") - end - end - def cast_and_validate(data) do data |> cast_data diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index 56b93dde8..a65fe2354 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:replies_count, :integer, default: 0) field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) - field(:inRepyTo, :string) + field(:inReplyTo, :string) field(:uri, ObjectValidators.Uri) field(:likes, {:array, :string}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex new file mode 100644 index 000000000..478b3b5cf --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + + embedded_schema do + field(:name, :string) + + embeds_one :replies, Replies, primary_key: false do + field(:totalItems, :integer) + field(:type, :string) + end + + field(:type, :string) + end + + def changeset(struct, data) do + struct + |> cast(data, [:name, :type]) + |> cast_embed(:replies, with: &replies_changeset/2) + |> validate_inclusion(:type, ["Note"]) + |> validate_required([:name, :type]) + end + + def replies_changeset(struct, data) do + struct + |> cast(data, [:totalItems, :type]) + |> validate_inclusion(:type, ["Collection"]) + |> validate_required([:type]) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex new file mode 100644 index 000000000..f47acf606 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator + alias Pleroma.Web.ActivityPub.Utils + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + # Extends from NoteValidator + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:to, {:array, :string}, default: []) + field(:cc, {:array, :string}, default: []) + field(:bto, {:array, :string}, default: []) + field(:bcc, {:array, :string}, default: []) + # TODO: Write type + field(:tag, {:array, :map}, default: []) + field(:type, :string) + field(:content, :string) + field(:context, :string) + + # TODO: Remove actor on objects + field(:actor, ObjectValidators.ObjectID) + + field(:attributedTo, ObjectValidators.ObjectID) + field(:summary, :string) + field(:published, ObjectValidators.DateTime) + # TODO: Write type + field(:emoji, :map, default: %{}) + field(:sensitive, :boolean, default: false) + embeds_many(:attachment, AttachmentValidator) + field(:replies_count, :integer, default: 0) + field(:like_count, :integer, default: 0) + field(:announcement_count, :integer, default: 0) + field(:inReplyTo, :string) + field(:uri, ObjectValidators.Uri) + # short identifier for PleromaFE to group statuses by context + field(:context_id, :integer) + + field(:likes, {:array, :string}, default: []) + field(:announcements, {:array, :string}, default: []) + + field(:closed, ObjectValidators.DateTime) + field(:voters, {:array, ObjectValidators.ObjectID}, default: []) + embeds_many(:anyOf, QuestionOptionsValidator) + embeds_many(:oneOf, QuestionOptionsValidator) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + defp fix_closed(data) do + cond do + is_binary(data["closed"]) -> data + is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"]) + true -> Map.drop(data, ["closed"]) + end + end + + # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults + defp fix_defaults(data) do + %{data: %{"id" => context}, id: context_id} = + Utils.create_context(data["context"] || data["conversation"]) + + data + |> Map.put_new_lazy("published", &Utils.make_date/0) + |> Map.put_new("context", context) + |> Map.put_new("context_id", context_id) + end + + defp fix_attribution(data) do + data + |> Map.put_new("actor", data["attributedTo"]) + end + + defp fix(data) do + data + |> fix_attribution() + |> fix_closed() + |> fix_defaults() + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment]) + |> cast_embed(:attachment) + |> cast_embed(:anyOf) + |> cast_embed(:oneOf) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["Question"]) + |> validate_required([:id, :actor, :attributedTo, :type, :context]) + |> CommonValidations.validate_any_presence([:cc, :to]) + |> CommonValidations.validate_fields_match([:actor, :attributedTo]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_any_presence([:oneOf, :anyOf]) + |> CommonValidations.validate_host_match() + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex index f64fac46d..881030f38 100644 --- a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do embedded_schema do field(:type, :string) field(:href, ObjectValidators.Uri) - field(:mediaType, :string) + field(:mediaType, :string, default: "application/octet-stream") end def changeset(struct, data) do diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 1d2c296a5..bcd6fd2fb 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do """ alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics + alias Pleroma.ActivityExpiration alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.FollowingRelationship @@ -15,13 +16,70 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Push alias Pleroma.Web.Streamer + alias Pleroma.Workers.BackgroundWorker + + require Logger def handle(object, meta \\ []) + # Task this handles + # - Follows + # - Sends a notification + def handle( + %{ + data: %{ + "actor" => actor, + "type" => "Accept", + "object" => follow_activity_id + } + } = object, + meta + ) do + with %Activity{actor: follower_id} = follow_activity <- + Activity.get_by_ap_id(follow_activity_id), + %User{} = followed <- User.get_cached_by_ap_id(actor), + %User{} = follower <- User.get_cached_by_ap_id(follower_id), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do + Notification.update_notification_type(followed, follow_activity) + User.update_follower_count(followed) + User.update_following_count(follower) + end + + {:ok, object, meta} + end + + # Task this handles + # - Rejects all existing follow activities for this person + # - Updates the follow state + # - Dismisses notification + def handle( + %{ + data: %{ + "actor" => actor, + "type" => "Reject", + "object" => follow_activity_id + } + } = object, + meta + ) do + with %Activity{actor: follower_id} = follow_activity <- + Activity.get_by_ap_id(follow_activity_id), + %User{} = followed <- User.get_cached_by_ap_id(actor), + %User{} = follower <- User.get_cached_by_ap_id(follower_id), + {:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do + FollowingRelationship.update(follower, followed, :follow_reject) + Notification.dismiss(follow_activity) + end + + {:ok, object, meta} + end + # Tasks this handle # - Follows if possible # - Sends a notification @@ -42,33 +100,13 @@ def handle( {_, {:ok, _}, _, _} <- {:following, User.follow(follower, followed, :follow_pending), follower, followed} do if followed.local && !followed.locked do - Utils.update_follow_state_for_all(object, "accept") - FollowingRelationship.update(follower, followed, :follow_accept) - User.update_follower_count(followed) - User.update_following_count(follower) - - %{ - to: [following_user], - actor: followed, - object: follow_id, - local: true - } - |> ActivityPub.accept() + {:ok, accept_data, _} = Builder.accept(followed, object) + {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true) end else - {:following, {:error, _}, follower, followed} -> - Utils.update_follow_state_for_all(object, "reject") - FollowingRelationship.update(follower, followed, :follow_reject) - - if followed.local do - %{ - to: [follower.ap_id], - actor: followed, - object: follow_id, - local: true - } - |> ActivityPub.reject() - end + {:following, {:error, _}, _follower, followed} -> + {:ok, reject_data, _} = Builder.reject(followed, object) + {:ok, _activity, _} = Pipeline.common_pipeline(reject_data, local: true) _ -> nil @@ -135,10 +173,26 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # Tasks this handles # - Actually create object # - Rollback if we couldn't create it + # - Increase the user note count + # - Increase the reply count + # - Increase replies count + # - Set up ActivityExpiration # - Set up notifications def handle(%{data: %{"type" => "Create"}} = activity, meta) do - with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do + with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta), + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do {:ok, notifications} = Notification.create_notifications(activity, do_send: false) + {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object) + + if in_reply_to = object.data["inReplyTo"] do + Object.increase_replies_count(in_reply_to) + end + + if expires_at = activity.data["expires_at"] do + ActivityExpiration.create(activity, expires_at) + end + + BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) meta = meta @@ -199,13 +253,15 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do # - Stream out the activity def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do deleted_object = - Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object) + Object.normalize(deleted_object, false) || + User.get_cached_by_ap_id(deleted_object) result = case deleted_object do %Object{} -> with {:ok, deleted_object, activity} <- Object.delete(deleted_object), - %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do + {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]}, + %User{} = user <- User.get_cached_by_ap_id(actor) do User.remove_pinnned_activity(user, activity) {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) @@ -219,6 +275,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, ActivityPub.stream_out(object) ActivityPub.stream_out_participations(deleted_object, user) :ok + else + {:actor, _} -> + Logger.error("The object doesn't have an actor: #{inspect(deleted_object)}") + :no_object_actor end %User{} -> @@ -268,9 +328,27 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do end end + def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do + Object.increase_vote_count( + object.data["inReplyTo"], + object.data["name"], + object.data["actor"] + ) + + {:ok, object, meta} + end + end + + def handle_object_creation(%{"type" => "Question"} = object, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + {:ok, object, meta} + end + end + # Nothing to do - def handle_object_creation(object) do - {:ok, object} + def handle_object_creation(object, meta) do + {:ok, object, meta} end defp undo_like(nil, object), do: delete_object(object) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 35aa05eb5..544f3f3b6 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,9 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.FollowingRelationship alias Pleroma.Maps - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -157,7 +155,12 @@ def fix_addressing(object) do end def fix_actor(%{"attributedTo" => actor} = object) do - Map.put(object, "actor", Containment.get_actor(%{"actor" => actor})) + actor = Containment.get_actor(%{"actor" => actor}) + + # TODO: Remove actor field for Objects + object + |> Map.put("actor", actor) + |> Map.put("attributedTo", actor) end def fix_in_reply_to(object, options \\ []) @@ -240,13 +243,17 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm if href do attachment_url = - %{"href" => href} + %{ + "href" => href, + "type" => Map.get(url || %{}, "type", "Link") + } |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", Map.get(url || %{}, "type")) - %{"url" => [attachment_url]} + %{ + "url" => [attachment_url], + "type" => data["type"] || "Document" + } |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", data["type"]) |> Maps.put_if_present("name", data["name"]) else nil @@ -382,32 +389,6 @@ defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = objec defp fix_content(object), do: object - defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do - with true <- id =~ "follows", - %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), - %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do - {:ok, activity} - else - _ -> {:error, nil} - end - end - - defp mastodon_follow_hack(_, _), do: {:error, nil} - - defp get_follow_activity(follow_object, followed) do - with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object), - {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do - {:ok, activity} - else - # Can't find the activity. This might a Mastodon 2.3 "Accept" - {:activity, nil} -> - mastodon_follow_hack(follow_object, followed) - - _ -> - {:error, nil} - end - end - # Reduce the object list to find the reported user. defp get_reported(objects) do Enum.reduce_while(objects, nil, fn ap_id, _ -> @@ -419,6 +400,29 @@ defp get_reported(objects) do end) end + # Compatibility wrapper for Mastodon votes + defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do + handle_incoming(data) + end + + defp handle_create(%{"object" => object} = data, user) do + %{ + to: data["to"], + object: object, + actor: user, + context: object["context"], + local: false, + published: data["published"], + additional: + Map.take(data, [ + "cc", + "directMessage", + "id" + ]) + } + |> ActivityPub.create() + end + def handle_incoming(data, options \\ []) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them @@ -457,30 +461,18 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do + when objtype in ["Article", "Event", "Note", "Video", "Page", "Audio"] do actor = Containment.get_actor(data) with nil <- Activity.get_create_by_object_ap_id(object["id"]), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor), - data <- Map.put(data, "actor", actor) |> fix_addressing() do - object = fix_object(object, options) + {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do + data = + data + |> Map.put("object", fix_object(object, options)) + |> Map.put("actor", actor) + |> fix_addressing() - params = %{ - to: data["to"], - object: object, - actor: user, - context: object["context"], - local: false, - published: data["published"], - additional: - Map.take(data, [ - "cc", - "directMessage", - "id" - ]) - } - - with {:ok, created_activity} <- ActivityPub.create(params) do + with {:ok, created_activity} <- handle_create(data, user) do reply_depth = (options[:depth] || 0) + 1 if Federator.allowed_thread_distance?(reply_depth) do @@ -531,60 +523,6 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, - _options - ) do - with actor <- Containment.get_actor(data), - {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), - {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), - %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do - User.update_follower_count(followed) - User.update_following_count(follower) - - Notification.update_notification_type(followed, follow_activity) - - ActivityPub.accept(%{ - to: follow_activity.data["to"], - type: "Accept", - actor: followed, - object: follow_activity.data["id"], - local: false, - activity_id: id - }) - else - _e -> - :error - end - end - - def handle_incoming( - %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data, - _options - ) do - with actor <- Containment.get_actor(data), - {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), - {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), - %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject), - {:ok, activity} <- - ActivityPub.reject(%{ - to: follow_activity.data["to"], - type: "Reject", - actor: followed, - object: follow_activity.data["id"], - local: false, - activity_id: id - }) do - {:ok, activity} - else - _e -> :error - end - end - @misskey_reactions %{ "like" => "👍", "love" => "❤️", @@ -614,9 +552,10 @@ def handle_incoming( end def handle_incoming( - %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, + %{"type" => "Create", "object" => %{"type" => objtype}} = data, _options - ) do + ) + when objtype in ["Question", "Answer", "ChatMessage"] do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -638,9 +577,10 @@ def handle_incoming( %{"type" => type} = data, _options ) - when type in ~w{Update Block Follow} do + when type in ~w{Update Block Follow Accept Reject} do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), - {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _} <- + Pipeline.common_pipeline(data, local: false) do {:ok, activity} end end @@ -649,7 +589,8 @@ def handle_incoming( %{"type" => "Delete"} = data, _options ) do - with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + with {:ok, activity, _} <- + Pipeline.common_pipeline(data, local: false) do {:ok, activity} else {:error, {:validate_object, _}} = e -> diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index f63a66c03..402ab428b 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -28,10 +28,6 @@ def get_user(%Plug.Conn{} = conn) do %User{} = user <- ldap_user(name, password) do {:ok, user} else - {:error, {:ldap_connection_error, _}} -> - # When LDAP is unavailable, try default authenticator - @base.get_user(conn) - {:ldap, _} -> @base.get_user(conn) @@ -92,7 +88,7 @@ defp bind_user(connection, ldap, name, password) do user _ -> - register_user(connection, base, uid, name, password) + register_user(connection, base, uid, name) end error -> @@ -100,34 +96,31 @@ defp bind_user(connection, ldap, name, password) do end end - defp register_user(connection, base, uid, name, password) do + defp register_user(connection, base, uid, name) do case :eldap.search(connection, [ {:base, to_charlist(base)}, {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, {:scope, :eldap.wholeSubtree()}, - {:attributes, ['mail', 'email']}, {:timeout, @search_timeout} ]) do {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} -> - with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do - params = %{ - email: :erlang.list_to_binary(mail), - name: name, - nickname: name, - password: password, - password_confirmation: password - } + params = %{ + name: name, + nickname: name, + password: nil + } - changeset = User.register_changeset(%User{}, params) - - case User.register(changeset) do - {:ok, user} -> user - error -> error + params = + case List.keyfind(attributes, 'mail', 0) do + {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) + _ -> params end - else - _ -> - Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}") - {:error, :ldap_registration_missing_attributes} + + changeset = User.register_changeset_ldap(%User{}, params) + + case User.register(changeset) do + {:ok, user} -> user + error -> error end error -> diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 4d5b0decf..a8141b28f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -6,9 +6,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation - alias Pleroma.FollowingRelationship alias Pleroma.Formatter - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User @@ -122,33 +120,16 @@ def unfollow(follower, unfollowed) do def accept_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, follower} <- User.follow(follower, followed), - {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept), - {:ok, _activity} <- - ActivityPub.accept(%{ - to: [follower.ap_id], - actor: followed, - object: follow_activity.data["id"], - type: "Accept" - }) do - Notification.update_notification_type(followed, follow_activity) + {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do {:ok, follower} end end def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject), - {:ok, _notifications} <- Notification.dismiss(follow_activity), - {:ok, _activity} <- - ActivityPub.reject(%{ - to: [follower.ap_id], - actor: followed, - object: follow_activity.data["id"], - type: "Reject" - }) do + {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do {:ok, follower} end end @@ -308,18 +289,19 @@ def vote(user, %{data: %{"type" => "Question"}} = object, choices) do {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do answer_activities = Enum.map(choices, fn index -> - answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + {:ok, answer_object, _meta} = + Builder.answer(user, object, Enum.at(options, index)["name"]) - {:ok, activity} = - ActivityPub.create(%{ - to: answer_data["to"], - actor: user, - context: object.data["context"], - object: answer_data, - additional: %{"cc" => answer_data["cc"]} - }) + {:ok, activity_data, _meta} = Builder.create(user, answer_object, []) - activity + {:ok, activity, _meta} = + activity_data + |> Map.put("cc", answer_object["cc"]) + |> Map.put("context", answer_object["context"]) + |> Pipeline.common_pipeline(local: true) + + # TODO: Do preload of Pleroma.Object in Pipeline + Activity.normalize(activity.data) end) object = Object.get_cached_by_ap_id(object.data["id"]) @@ -340,8 +322,13 @@ defp validate_existing_votes(%{ap_id: ap_id}, object) do end end - defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)} - defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1} + defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}) + when is_list(any_of) and any_of != [], + do: {any_of, Enum.count(any_of)} + + defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}) + when is_list(one_of) and one_of != [], + do: {one_of, 1} defp normalize_and_validate_choices(choices, object) do choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 9c38b73eb..9d7b24eb2 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -548,17 +548,6 @@ def conversation_id_to_context(id) do end end - def make_answer_data(%User{ap_id: ap_id}, object, name) do - %{ - "type" => "Answer", - "actor" => ap_id, - "cc" => [object.data["actor"]], - "to" => [], - "name" => name, - "inReplyTo" => object.data["id"] - } - end - def validate_character_limit("" = _full_payload, [] = _attachments) do {:error, dgettext("errors", "Cannot post an empty status without attachments")} end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 69946fb81..6445966e0 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -18,6 +18,12 @@ def falsy_param?(value), def truthy_param?(value), do: not falsy_param?(value) + def json_response(conn, status, _) when status in [204, :no_content] do + conn + |> put_resp_header("content-type", "application/json") + |> send_resp(status, "") + end + def json_response(conn, status, json) do conn |> put_status(status) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index f45678184..95d8452df 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -226,7 +226,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p with changeset <- User.update_changeset(user, user_params), {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update), updated_object <- - Pleroma.Web.ActivityPub.UserView.render("user.json", user: user) + Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user) |> Map.delete("@context"), {:ok, update_data, []} <- Builder.update(user, updated_object), {:ok, _update, _} <- diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex index aeff646f5..c37f624e0 100644 --- a/lib/pleroma/web/mastodon_api/views/filter_view.ex +++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex @@ -25,7 +25,7 @@ def render("show.json", %{filter: filter}) do context: filter.context, expires_at: expires_at, irreversible: filter.hide, - whole_word: false + whole_word: filter.whole_word } end end diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 59a5deb28..1208dc9a0 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -28,10 +28,10 @@ def render("show.json", %{object: object, multiple: multiple, options: options} def render("show.json", %{object: object} = params) do case object.data do - %{"anyOf" => options} when is_list(options) -> + %{"anyOf" => [_ | _] = options} -> render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options})) - %{"oneOf" => options} when is_list(options) -> + %{"oneOf" => [_ | _] = options} -> render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options})) _ -> @@ -40,15 +40,13 @@ def render("show.json", %{object: object} = params) do end defp end_time_and_expired(object) do - case object.data["closed"] || object.data["endTime"] do - end_time when is_binary(end_time) -> - end_time = NaiveDateTime.from_iso8601!(end_time) - expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt + if object.data["closed"] do + end_time = NaiveDateTime.from_iso8601!(object.data["closed"]) + expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt - {Utils.to_masto_date(end_time), expired} - - _ -> - {nil, false} + {Utils.to_masto_date(end_time), expired} + else + {nil, false} end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index f29b3cb57..dd00600ea 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -76,6 +76,13 @@ defp do_authorize(%Plug.Conn{} = conn, params) do available_scopes = (app && app.scopes) || [] scopes = Scopes.fetch_scopes(params, available_scopes) + scopes = + if scopes == [] do + available_scopes + else + scopes + end + # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template render(conn, Authenticator.auth_template(), %{ response_type: params["response_type"], diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 5c7daf1a5..6210f2c5a 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -9,6 +9,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser + @rich_media_options [ + pool: :media, + max_body: 2_000_000 + ] + @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld]) @@ -77,4 +82,20 @@ def perform(:fetch, %Activity{} = activity) do fetch_data_for_activity(activity) :ok end + + def rich_media_get(url) do + headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] + + options = + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do + Keyword.merge(@rich_media_options, + recv_timeout: 2_000, + with_body: true + ) + else + @rich_media_options + end + + Pleroma.HTTP.get(url, headers, options) + end end diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index c8a767935..ca592833f 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -3,11 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parser do - @options [ - pool: :media, - max_body: 2_000_000 - ] - defp parsers do Pleroma.Config.get([:rich_media, :parsers]) end @@ -75,21 +70,8 @@ defp get_ttl_from_image(data, url) do end defp parse_url(url) do - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.merge(@options, - recv_timeout: 2_000, - with_body: true - ) - else - @options - end - try do - rich_media_agent = Pleroma.Application.user_agent() <> "; Bot" - - {:ok, %Tesla.Env{body: html}} = - Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts) + {:ok, %Tesla.Env{body: html}} = Pleroma.Web.RichMedia.Helpers.rich_media_get(url) html |> parse_html() diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 6bdeac89c..1fe6729c3 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -22,7 +22,7 @@ defp get_oembed_url([{"link", attributes, _children} | _]) do end defp get_oembed_data(url) do - with {:ok, %Tesla.Env{body: json}} <- Pleroma.HTTP.get(url, [], adapter: [pool: :media]) do + with {:ok, %Tesla.Env{body: json}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url) do Jason.decode(json) end end diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 5836ec1e0..51603fe0c 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -37,7 +37,7 @@ } a { - color: color: #d8a070; + color: #d8a070; text-decoration: none; } diff --git a/mix.exs b/mix.exs index 0e723c15f..11fdb1670 100644 --- a/mix.exs +++ b/mix.exs @@ -127,7 +127,7 @@ defp deps do {:pbkdf2_elixir, "~> 1.2"}, {:bcrypt_elixir, "~> 2.2"}, {:trailing_format_plug, "~> 0.0.7"}, - {:fast_sanitize, "~> 0.1"}, + {:fast_sanitize, "~> 0.2.0"}, {:html_entities, "~> 0.5", override: true}, {:phoenix_html, "~> 2.14"}, {:calendar, "~> 1.0"}, @@ -214,7 +214,8 @@ defp aliases do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"], - docs: ["pleroma.docs", "docs"] + docs: ["pleroma.docs", "docs"], + analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"] ] end @@ -228,10 +229,10 @@ defp aliases do defp version(version) do identifier_filter = ~r/[^0-9a-z\-]+/i - {_cmdgit, cmdgit_err} = System.cmd("sh", ["-c", "command -v git"]) + git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"])) git_pre_release = - if cmdgit_err == 0 do + if git_available? do {tag, tag_err} = System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) @@ -257,7 +258,7 @@ defp version(version) do # Branch name as pre-release version component, denoted with a dot branch_name = - with 0 <- cmdgit_err, + with true <- git_available?, {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, diff --git a/mix.lock b/mix.lock index 55c3c59c6..7ec3a0b28 100644 --- a/mix.lock +++ b/mix.lock @@ -42,8 +42,8 @@ "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, - "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"}, - "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"}, + "fast_html": {:hex, :fast_html, "2.0.1", "e126c74d287768ae78c48938da6711164517300d108a78f8a38993df8d588335", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "bdd6f8525c95ad391a4f10d9a1b3da4cea94078ec8638487aa8c24015ad9393a"}, + "fast_sanitize": {:hex, :fast_sanitize, "0.2.0", "004b40d5bbecda182b6fdba762a51fffd3501e689e8eafe196e1a97eb0caf733", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "11fcb37f26d272a3a2aff861872bf100be4eeacea69505908b8cdbcea5b0813a"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, @@ -76,6 +76,7 @@ "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, + "nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"}, "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]}, diff --git a/priv/repo/migrations/20200802170532_fix_legacy_tags.exs b/priv/repo/migrations/20200802170532_fix_legacy_tags.exs new file mode 100644 index 000000000..ca82fac42 --- /dev/null +++ b/priv/repo/migrations/20200802170532_fix_legacy_tags.exs @@ -0,0 +1,40 @@ +# Fix legacy tags set by AdminFE that don't align with TagPolicy MRF + +defmodule Pleroma.Repo.Migrations.FixLegacyTags do + use Ecto.Migration + alias Pleroma.Repo + alias Pleroma.User + import Ecto.Query + + @old_new_map %{ + "force_nsfw" => "mrf_tag:media-force-nsfw", + "strip_media" => "mrf_tag:media-strip", + "force_unlisted" => "mrf_tag:force-unlisted", + "sandbox" => "mrf_tag:sandbox", + "disable_remote_subscription" => "mrf_tag:disable-remote-subscription", + "disable_any_subscription" => "mrf_tag:disable-any-subscription" + } + + def change do + legacy_tags = Map.keys(@old_new_map) + + from(u in User, + where: fragment("? && ?", u.tags, ^legacy_tags), + select: struct(u, [:tags, :id]) + ) + |> Repo.chunk_stream(100) + |> Enum.each(fn user -> + fix_tags_changeset(user) + |> Repo.update() + end) + end + + defp fix_tags_changeset(%User{tags: tags} = user) do + new_tags = + Enum.map(tags, fn tag -> + Map.get(@old_new_map, tag, tag) + end) + + Ecto.Changeset.change(user, tags: new_tags) + end +end diff --git a/priv/repo/migrations/20200804180322_remove_nonlocal_expirations.exs b/priv/repo/migrations/20200804180322_remove_nonlocal_expirations.exs new file mode 100644 index 000000000..389935f0d --- /dev/null +++ b/priv/repo/migrations/20200804180322_remove_nonlocal_expirations.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RemoveNonlocalExpirations do + use Ecto.Migration + + def up do + statement = """ + DELETE FROM + activity_expirations A USING activities B + WHERE + A.activity_id = B.id + AND B.local = false; + """ + + execute(statement) + end + + def down do + :ok + end +end diff --git a/priv/repo/migrations/20200804183107_add_unique_index_to_app_client_id.exs b/priv/repo/migrations/20200804183107_add_unique_index_to_app_client_id.exs new file mode 100644 index 000000000..83de18096 --- /dev/null +++ b/priv/repo/migrations/20200804183107_add_unique_index_to_app_client_id.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddUniqueIndexToAppClientId do + use Ecto.Migration + + def change do + create(unique_index(:apps, [:client_id])) + end +end diff --git a/priv/repo/migrations/20200808173046_only_expire_creates.exs b/priv/repo/migrations/20200808173046_only_expire_creates.exs new file mode 100644 index 000000000..9df52956f --- /dev/null +++ b/priv/repo/migrations/20200808173046_only_expire_creates.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.OnlyExpireCreates do + use Ecto.Migration + + def up do + statement = """ + DELETE FROM + activity_expirations a_exp USING activities a, objects o + WHERE + a_exp.activity_id = a.id AND (o.data->>'id') = COALESCE(a.data->'object'->>'id', a.data->>'object') + AND (a.data->>'type' != 'Create' OR o.data->>'type' != 'Note'); + """ + + execute(statement) + end + + def down do + :ok + end +end diff --git a/priv/repo/migrations/20200811125613_set_defaults_to_user_approval_pending.exs b/priv/repo/migrations/20200811125613_set_defaults_to_user_approval_pending.exs new file mode 100644 index 000000000..eec7da03f --- /dev/null +++ b/priv/repo/migrations/20200811125613_set_defaults_to_user_approval_pending.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetDefaultsToUserApprovalPending do + use Ecto.Migration + + def up do + execute("UPDATE users SET approval_pending = false WHERE approval_pending IS NULL") + + alter table(:users) do + modify(:approval_pending, :boolean, default: false, null: false) + end + end + + def down do + :ok + end +end diff --git a/priv/repo/migrations/20200811143147_ap_id_not_null.exs b/priv/repo/migrations/20200811143147_ap_id_not_null.exs new file mode 100644 index 000000000..df649c7ca --- /dev/null +++ b/priv/repo/migrations/20200811143147_ap_id_not_null.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.ApIdNotNull do + use Ecto.Migration + + require Logger + + def up do + Logger.warn( + "If this migration fails please open an issue at https://git.pleroma.social/pleroma/pleroma/-/issues/new \n" + ) + + alter table(:users) do + modify(:ap_id, :string, null: false) + end + end + + def down do + :ok + end +end diff --git a/priv/static/index.html b/priv/static/index.html index 2257dec35..7dd080b2d 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/css/app.6dbc7dea4fc148c85860.css b/priv/static/static/css/app.77b1644622e3bae24b6b.css similarity index 99% rename from priv/static/static/css/app.6dbc7dea4fc148c85860.css rename to priv/static/static/css/app.77b1644622e3bae24b6b.css index 3927e3b77..8038882c0 100644 Binary files a/priv/static/static/css/app.6dbc7dea4fc148c85860.css and b/priv/static/static/css/app.77b1644622e3bae24b6b.css differ diff --git a/priv/static/static/css/app.6dbc7dea4fc148c85860.css.map b/priv/static/static/css/app.77b1644622e3bae24b6b.css.map similarity index 98% rename from priv/static/static/css/app.6dbc7dea4fc148c85860.css.map rename to priv/static/static/css/app.77b1644622e3bae24b6b.css.map index 963d5b3b8..4b042ef35 100644 --- a/priv/static/static/css/app.6dbc7dea4fc148c85860.css.map +++ b/priv/static/static/css/app.77b1644622e3bae24b6b.css.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.6dbc7dea4fc148c85860.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n font-size: 2em;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 1em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.77b1644622e3bae24b6b.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n font-size: 2em;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 1em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/fontello.1594823398494.woff2 b/priv/static/static/font/fontello.1594823398494.woff2 deleted file mode 100644 index c61bf111a..000000000 Binary files a/priv/static/static/font/fontello.1594823398494.woff2 and /dev/null differ diff --git a/priv/static/static/font/fontello.1594823398494.eot b/priv/static/static/font/fontello.1597327457363.eot similarity index 99% rename from priv/static/static/font/fontello.1594823398494.eot rename to priv/static/static/font/fontello.1597327457363.eot index 12e6beabf..af2c39275 100644 Binary files a/priv/static/static/font/fontello.1594823398494.eot and b/priv/static/static/font/fontello.1597327457363.eot differ diff --git a/priv/static/static/font/fontello.1594823398494.svg b/priv/static/static/font/fontello.1597327457363.svg similarity index 100% rename from priv/static/static/font/fontello.1594823398494.svg rename to priv/static/static/font/fontello.1597327457363.svg diff --git a/priv/static/static/font/fontello.1594823398494.ttf b/priv/static/static/font/fontello.1597327457363.ttf similarity index 99% rename from priv/static/static/font/fontello.1594823398494.ttf rename to priv/static/static/font/fontello.1597327457363.ttf index 6f21845a8..1d5640d5d 100644 Binary files a/priv/static/static/font/fontello.1594823398494.ttf and b/priv/static/static/font/fontello.1597327457363.ttf differ diff --git a/priv/static/static/font/fontello.1594823398494.woff b/priv/static/static/font/fontello.1597327457363.woff similarity index 98% rename from priv/static/static/font/fontello.1594823398494.woff rename to priv/static/static/font/fontello.1597327457363.woff index a7cd098f4..c04735bf5 100644 Binary files a/priv/static/static/font/fontello.1594823398494.woff and b/priv/static/static/font/fontello.1597327457363.woff differ diff --git a/priv/static/static/font/fontello.1597327457363.woff2 b/priv/static/static/font/fontello.1597327457363.woff2 new file mode 100644 index 000000000..f53414761 Binary files /dev/null and b/priv/static/static/font/fontello.1597327457363.woff2 differ diff --git a/priv/static/static/fontello.1597327457363.css b/priv/static/static/fontello.1597327457363.css new file mode 100644 index 000000000..22d148873 Binary files /dev/null and b/priv/static/static/fontello.1597327457363.css differ diff --git a/priv/static/static/js/10.5ef4671883649cf93524.js b/priv/static/static/js/10.8c5b75840b696a152c7e.js similarity index 99% rename from priv/static/static/js/10.5ef4671883649cf93524.js rename to priv/static/static/js/10.8c5b75840b696a152c7e.js index 6819c854b..eb95d66d1 100644 Binary files a/priv/static/static/js/10.5ef4671883649cf93524.js and b/priv/static/static/js/10.8c5b75840b696a152c7e.js differ diff --git a/priv/static/static/js/10.5ef4671883649cf93524.js.map b/priv/static/static/js/10.8c5b75840b696a152c7e.js.map similarity index 56% rename from priv/static/static/js/10.5ef4671883649cf93524.js.map rename to priv/static/static/js/10.8c5b75840b696a152c7e.js.map index 95fa2207e..b390fbeaf 100644 Binary files a/priv/static/static/js/10.5ef4671883649cf93524.js.map and b/priv/static/static/js/10.8c5b75840b696a152c7e.js.map differ diff --git a/priv/static/static/js/11.c5b938b4349f87567338.js b/priv/static/static/js/11.bfcde1c26c4d54b84ee4.js similarity index 99% rename from priv/static/static/js/11.c5b938b4349f87567338.js rename to priv/static/static/js/11.bfcde1c26c4d54b84ee4.js index b97f69bf3..0dea63f5a 100644 Binary files a/priv/static/static/js/11.c5b938b4349f87567338.js and b/priv/static/static/js/11.bfcde1c26c4d54b84ee4.js differ diff --git a/priv/static/static/js/11.c5b938b4349f87567338.js.map b/priv/static/static/js/11.bfcde1c26c4d54b84ee4.js.map similarity index 56% rename from priv/static/static/js/11.c5b938b4349f87567338.js.map rename to priv/static/static/js/11.bfcde1c26c4d54b84ee4.js.map index 5ccf83b1d..2b2305773 100644 Binary files a/priv/static/static/js/11.c5b938b4349f87567338.js.map and b/priv/static/static/js/11.bfcde1c26c4d54b84ee4.js.map differ diff --git a/priv/static/static/js/12.76095ee23394e0ef65bb.js b/priv/static/static/js/12.76095ee23394e0ef65bb.js new file mode 100644 index 000000000..6c34e2da2 Binary files /dev/null and b/priv/static/static/js/12.76095ee23394e0ef65bb.js differ diff --git a/priv/static/static/js/12.ab82f9512fa85e78c114.js.map b/priv/static/static/js/12.76095ee23394e0ef65bb.js.map similarity index 56% rename from priv/static/static/js/12.ab82f9512fa85e78c114.js.map rename to priv/static/static/js/12.76095ee23394e0ef65bb.js.map index 23335ae23..e00137a2b 100644 Binary files a/priv/static/static/js/12.ab82f9512fa85e78c114.js.map and b/priv/static/static/js/12.76095ee23394e0ef65bb.js.map differ diff --git a/priv/static/static/js/12.ab82f9512fa85e78c114.js b/priv/static/static/js/12.ab82f9512fa85e78c114.js deleted file mode 100644 index 100d72b33..000000000 Binary files a/priv/static/static/js/12.ab82f9512fa85e78c114.js and /dev/null differ diff --git a/priv/static/static/js/13.40e59c5015d3307b94ad.js b/priv/static/static/js/13.40e59c5015d3307b94ad.js deleted file mode 100644 index 2088bb6b7..000000000 Binary files a/priv/static/static/js/13.40e59c5015d3307b94ad.js and /dev/null differ diff --git a/priv/static/static/js/13.957b04ac11d6cde66f5b.js b/priv/static/static/js/13.957b04ac11d6cde66f5b.js new file mode 100644 index 000000000..917b6a58b Binary files /dev/null and b/priv/static/static/js/13.957b04ac11d6cde66f5b.js differ diff --git a/priv/static/static/js/13.40e59c5015d3307b94ad.js.map b/priv/static/static/js/13.957b04ac11d6cde66f5b.js.map similarity index 56% rename from priv/static/static/js/13.40e59c5015d3307b94ad.js.map rename to priv/static/static/js/13.957b04ac11d6cde66f5b.js.map index 3931b5ef9..25434f73b 100644 Binary files a/priv/static/static/js/13.40e59c5015d3307b94ad.js.map and b/priv/static/static/js/13.957b04ac11d6cde66f5b.js.map differ diff --git a/priv/static/static/js/14.de791a47ee5249a526b1.js b/priv/static/static/js/14.aae5a904931591edfaa7.js similarity index 54% rename from priv/static/static/js/14.de791a47ee5249a526b1.js rename to priv/static/static/js/14.aae5a904931591edfaa7.js index 0e341275e..001914ad7 100644 Binary files a/priv/static/static/js/14.de791a47ee5249a526b1.js and b/priv/static/static/js/14.aae5a904931591edfaa7.js differ diff --git a/priv/static/static/js/14.aae5a904931591edfaa7.js.map b/priv/static/static/js/14.aae5a904931591edfaa7.js.map new file mode 100644 index 000000000..24719fee8 Binary files /dev/null and b/priv/static/static/js/14.aae5a904931591edfaa7.js.map differ diff --git a/priv/static/static/js/14.de791a47ee5249a526b1.js.map b/priv/static/static/js/14.de791a47ee5249a526b1.js.map deleted file mode 100644 index 4bef54546..000000000 Binary files a/priv/static/static/js/14.de791a47ee5249a526b1.js.map and /dev/null differ diff --git a/priv/static/static/js/15.139f5de3950adc3b66df.js b/priv/static/static/js/15.139f5de3950adc3b66df.js new file mode 100644 index 000000000..303e00130 Binary files /dev/null and b/priv/static/static/js/15.139f5de3950adc3b66df.js differ diff --git a/priv/static/static/js/15.139f5de3950adc3b66df.js.map b/priv/static/static/js/15.139f5de3950adc3b66df.js.map new file mode 100644 index 000000000..d5a3c800d Binary files /dev/null and b/priv/static/static/js/15.139f5de3950adc3b66df.js.map differ diff --git a/priv/static/static/js/15.e24854297ad682aec45a.js b/priv/static/static/js/15.e24854297ad682aec45a.js deleted file mode 100644 index 671370192..000000000 Binary files a/priv/static/static/js/15.e24854297ad682aec45a.js and /dev/null differ diff --git a/priv/static/static/js/15.e24854297ad682aec45a.js.map b/priv/static/static/js/15.e24854297ad682aec45a.js.map deleted file mode 100644 index 89789a542..000000000 Binary files a/priv/static/static/js/15.e24854297ad682aec45a.js.map and /dev/null differ diff --git a/priv/static/static/js/16.7b8466d62084c04f6671.js b/priv/static/static/js/16.7b8466d62084c04f6671.js new file mode 100644 index 000000000..587b41dd0 Binary files /dev/null and b/priv/static/static/js/16.7b8466d62084c04f6671.js differ diff --git a/priv/static/static/js/16.7b8466d62084c04f6671.js.map b/priv/static/static/js/16.7b8466d62084c04f6671.js.map new file mode 100644 index 000000000..22818639f Binary files /dev/null and b/priv/static/static/js/16.7b8466d62084c04f6671.js.map differ diff --git a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js deleted file mode 100644 index 6a3ea9513..000000000 Binary files a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js and /dev/null differ diff --git a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map deleted file mode 100644 index fec45b087..000000000 Binary files a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map and /dev/null differ diff --git a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map b/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map deleted file mode 100644 index 0c20fc89b..000000000 Binary files a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map and /dev/null differ diff --git a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js b/priv/static/static/js/17.e8ec1f5666cb4e28784a.js similarity index 94% rename from priv/static/static/js/17.c98118b6bb84ee3b5b08.js rename to priv/static/static/js/17.e8ec1f5666cb4e28784a.js index c41f0b6b8..03a7d28e5 100644 Binary files a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js and b/priv/static/static/js/17.e8ec1f5666cb4e28784a.js differ diff --git a/priv/static/static/js/17.e8ec1f5666cb4e28784a.js.map b/priv/static/static/js/17.e8ec1f5666cb4e28784a.js.map new file mode 100644 index 000000000..0fe92287e Binary files /dev/null and b/priv/static/static/js/17.e8ec1f5666cb4e28784a.js.map differ diff --git a/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map b/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map deleted file mode 100644 index 72cdf0e0e..000000000 Binary files a/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map and /dev/null differ diff --git a/priv/static/static/js/18.89c20aa67a4dd067ea37.js b/priv/static/static/js/18.d32389579b85948022b8.js similarity index 57% rename from priv/static/static/js/18.89c20aa67a4dd067ea37.js rename to priv/static/static/js/18.d32389579b85948022b8.js index db1c78a49..477f6e06f 100644 Binary files a/priv/static/static/js/18.89c20aa67a4dd067ea37.js and b/priv/static/static/js/18.d32389579b85948022b8.js differ diff --git a/priv/static/static/js/18.d32389579b85948022b8.js.map b/priv/static/static/js/18.d32389579b85948022b8.js.map new file mode 100644 index 000000000..62fc5b84f Binary files /dev/null and b/priv/static/static/js/18.d32389579b85948022b8.js.map differ diff --git a/priv/static/static/js/19.6e13bad8131c4501c1c5.js b/priv/static/static/js/19.6e13bad8131c4501c1c5.js deleted file mode 100644 index 8b32827cc..000000000 Binary files a/priv/static/static/js/19.6e13bad8131c4501c1c5.js and /dev/null differ diff --git a/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map b/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map deleted file mode 100644 index 762d85e27..000000000 Binary files a/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map and /dev/null differ diff --git a/priv/static/static/js/19.d180c594b843c17c80fa.js b/priv/static/static/js/19.d180c594b843c17c80fa.js new file mode 100644 index 000000000..c30dc75c2 Binary files /dev/null and b/priv/static/static/js/19.d180c594b843c17c80fa.js differ diff --git a/priv/static/static/js/19.d180c594b843c17c80fa.js.map b/priv/static/static/js/19.d180c594b843c17c80fa.js.map new file mode 100644 index 000000000..e90081dd9 Binary files /dev/null and b/priv/static/static/js/19.d180c594b843c17c80fa.js.map differ diff --git a/priv/static/static/js/2.5ecefab707beea40b7f0.js b/priv/static/static/js/2.5ecefab707beea40b7f0.js new file mode 100644 index 000000000..bf563c79f Binary files /dev/null and b/priv/static/static/js/2.5ecefab707beea40b7f0.js differ diff --git a/priv/static/static/js/2.5ecefab707beea40b7f0.js.map b/priv/static/static/js/2.5ecefab707beea40b7f0.js.map new file mode 100644 index 000000000..7452e1f6e Binary files /dev/null and b/priv/static/static/js/2.5ecefab707beea40b7f0.js.map differ diff --git a/priv/static/static/js/2.78a48aa26599b00c3b8d.js b/priv/static/static/js/2.78a48aa26599b00c3b8d.js deleted file mode 100644 index ecb27aa9c..000000000 Binary files a/priv/static/static/js/2.78a48aa26599b00c3b8d.js and /dev/null differ diff --git a/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map b/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map deleted file mode 100644 index 167cfa1c6..000000000 Binary files a/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map and /dev/null differ diff --git a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js b/priv/static/static/js/20.27e04f2209628de3092b.js similarity index 99% rename from priv/static/static/js/20.3615c3cea2e1c2707a4f.js rename to priv/static/static/js/20.27e04f2209628de3092b.js index 74f89016c..e41b60066 100644 Binary files a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js and b/priv/static/static/js/20.27e04f2209628de3092b.js differ diff --git a/priv/static/static/js/20.27e04f2209628de3092b.js.map b/priv/static/static/js/20.27e04f2209628de3092b.js.map new file mode 100644 index 000000000..4009ef5b9 Binary files /dev/null and b/priv/static/static/js/20.27e04f2209628de3092b.js.map differ diff --git a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map b/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map deleted file mode 100644 index acddecea7..000000000 Binary files a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map and /dev/null differ diff --git a/priv/static/static/js/21.64dedfc646e13e6f7915.js b/priv/static/static/js/21.641aba6f96885c381070.js similarity index 99% rename from priv/static/static/js/21.64dedfc646e13e6f7915.js rename to priv/static/static/js/21.641aba6f96885c381070.js index 407e6665e..d80f64e11 100644 Binary files a/priv/static/static/js/21.64dedfc646e13e6f7915.js and b/priv/static/static/js/21.641aba6f96885c381070.js differ diff --git a/priv/static/static/js/21.641aba6f96885c381070.js.map b/priv/static/static/js/21.641aba6f96885c381070.js.map new file mode 100644 index 000000000..8f6253113 Binary files /dev/null and b/priv/static/static/js/21.641aba6f96885c381070.js.map differ diff --git a/priv/static/static/js/21.64dedfc646e13e6f7915.js.map b/priv/static/static/js/21.64dedfc646e13e6f7915.js.map deleted file mode 100644 index 8e3432668..000000000 Binary files a/priv/static/static/js/21.64dedfc646e13e6f7915.js.map and /dev/null differ diff --git a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map b/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map deleted file mode 100644 index 1c556f040..000000000 Binary files a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map and /dev/null differ diff --git a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js b/priv/static/static/js/22.cbe4790c7601004ed96f.js similarity index 99% rename from priv/static/static/js/22.6fa63bc6a054b7638e9e.js rename to priv/static/static/js/22.cbe4790c7601004ed96f.js index 4a8740c99..0e9c6ab97 100644 Binary files a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js and b/priv/static/static/js/22.cbe4790c7601004ed96f.js differ diff --git a/priv/static/static/js/22.cbe4790c7601004ed96f.js.map b/priv/static/static/js/22.cbe4790c7601004ed96f.js.map new file mode 100644 index 000000000..8de20817c Binary files /dev/null and b/priv/static/static/js/22.cbe4790c7601004ed96f.js.map differ diff --git a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js b/priv/static/static/js/23.96b5bf8d37de3bf02a17.js similarity index 99% rename from priv/static/static/js/23.e0ddea2b6e049d221ee7.js rename to priv/static/static/js/23.96b5bf8d37de3bf02a17.js index 51fe36368..6a78c71fd 100644 Binary files a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js and b/priv/static/static/js/23.96b5bf8d37de3bf02a17.js differ diff --git a/priv/static/static/js/23.96b5bf8d37de3bf02a17.js.map b/priv/static/static/js/23.96b5bf8d37de3bf02a17.js.map new file mode 100644 index 000000000..12929720a Binary files /dev/null and b/priv/static/static/js/23.96b5bf8d37de3bf02a17.js.map differ diff --git a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map b/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map deleted file mode 100644 index 36bae2bf4..000000000 Binary files a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map and /dev/null differ diff --git a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map b/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map deleted file mode 100644 index 09f3c19d0..000000000 Binary files a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map and /dev/null differ diff --git a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js b/priv/static/static/js/24.5e5eea3542b0e17c6479.js similarity index 99% rename from priv/static/static/js/24.38e3b9d44e9ee703ebf6.js rename to priv/static/static/js/24.5e5eea3542b0e17c6479.js index e5abf0af6..45787dddd 100644 Binary files a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js and b/priv/static/static/js/24.5e5eea3542b0e17c6479.js differ diff --git a/priv/static/static/js/24.5e5eea3542b0e17c6479.js.map b/priv/static/static/js/24.5e5eea3542b0e17c6479.js.map new file mode 100644 index 000000000..1938ee57a Binary files /dev/null and b/priv/static/static/js/24.5e5eea3542b0e17c6479.js.map differ diff --git a/priv/static/static/js/25.696b41c0a8660e1f85af.js b/priv/static/static/js/25.696b41c0a8660e1f85af.js deleted file mode 100644 index b114890fc..000000000 Binary files a/priv/static/static/js/25.696b41c0a8660e1f85af.js and /dev/null differ diff --git a/priv/static/static/js/25.696b41c0a8660e1f85af.js.map b/priv/static/static/js/25.696b41c0a8660e1f85af.js.map deleted file mode 100644 index f6d208812..000000000 Binary files a/priv/static/static/js/25.696b41c0a8660e1f85af.js.map and /dev/null differ diff --git a/priv/static/static/js/25.dd8471a33b5a4d256564.js b/priv/static/static/js/25.dd8471a33b5a4d256564.js new file mode 100644 index 000000000..b30f01f9b Binary files /dev/null and b/priv/static/static/js/25.dd8471a33b5a4d256564.js differ diff --git a/priv/static/static/js/25.dd8471a33b5a4d256564.js.map b/priv/static/static/js/25.dd8471a33b5a4d256564.js.map new file mode 100644 index 000000000..e6a6bf3a0 Binary files /dev/null and b/priv/static/static/js/25.dd8471a33b5a4d256564.js.map differ diff --git a/priv/static/static/js/26.1168f22384be75dc5492.js.map b/priv/static/static/js/26.1168f22384be75dc5492.js.map deleted file mode 100644 index c9b0d8495..000000000 Binary files a/priv/static/static/js/26.1168f22384be75dc5492.js.map and /dev/null differ diff --git a/priv/static/static/js/26.1168f22384be75dc5492.js b/priv/static/static/js/26.91a9c2effdd1a423a79f.js similarity index 99% rename from priv/static/static/js/26.1168f22384be75dc5492.js rename to priv/static/static/js/26.91a9c2effdd1a423a79f.js index b77a4d30f..f30ff939a 100644 Binary files a/priv/static/static/js/26.1168f22384be75dc5492.js and b/priv/static/static/js/26.91a9c2effdd1a423a79f.js differ diff --git a/priv/static/static/js/26.91a9c2effdd1a423a79f.js.map b/priv/static/static/js/26.91a9c2effdd1a423a79f.js.map new file mode 100644 index 000000000..ae4781108 Binary files /dev/null and b/priv/static/static/js/26.91a9c2effdd1a423a79f.js.map differ diff --git a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map b/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map deleted file mode 100644 index 0cc5f46b2..000000000 Binary files a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map and /dev/null differ diff --git a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js b/priv/static/static/js/27.949d608895f6e29a2fc2.js similarity index 94% rename from priv/static/static/js/27.3c0cfbb2a898b35486dd.js rename to priv/static/static/js/27.949d608895f6e29a2fc2.js index a0765356f..f735c1a04 100644 Binary files a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js and b/priv/static/static/js/27.949d608895f6e29a2fc2.js differ diff --git a/priv/static/static/js/27.949d608895f6e29a2fc2.js.map b/priv/static/static/js/27.949d608895f6e29a2fc2.js.map new file mode 100644 index 000000000..9f75161dd Binary files /dev/null and b/priv/static/static/js/27.949d608895f6e29a2fc2.js.map differ diff --git a/priv/static/static/js/28.926c71d6f1813e177271.js b/priv/static/static/js/28.1e879ccb6222c26ee837.js similarity index 99% rename from priv/static/static/js/28.926c71d6f1813e177271.js rename to priv/static/static/js/28.1e879ccb6222c26ee837.js index 55cf840f2..64e286799 100644 Binary files a/priv/static/static/js/28.926c71d6f1813e177271.js and b/priv/static/static/js/28.1e879ccb6222c26ee837.js differ diff --git a/priv/static/static/js/28.1e879ccb6222c26ee837.js.map b/priv/static/static/js/28.1e879ccb6222c26ee837.js.map new file mode 100644 index 000000000..123aae91b Binary files /dev/null and b/priv/static/static/js/28.1e879ccb6222c26ee837.js.map differ diff --git a/priv/static/static/js/28.926c71d6f1813e177271.js.map b/priv/static/static/js/28.926c71d6f1813e177271.js.map deleted file mode 100644 index 1ae8f08cb..000000000 Binary files a/priv/static/static/js/28.926c71d6f1813e177271.js.map and /dev/null differ diff --git a/priv/static/static/js/29.187064ebed099ae45749.js.map b/priv/static/static/js/29.187064ebed099ae45749.js.map deleted file mode 100644 index b5dd63f96..000000000 Binary files a/priv/static/static/js/29.187064ebed099ae45749.js.map and /dev/null differ diff --git a/priv/static/static/js/29.187064ebed099ae45749.js b/priv/static/static/js/29.a0eb0eee98462dc00d86.js similarity index 99% rename from priv/static/static/js/29.187064ebed099ae45749.js rename to priv/static/static/js/29.a0eb0eee98462dc00d86.js index 6eaae0226..740e150ca 100644 Binary files a/priv/static/static/js/29.187064ebed099ae45749.js and b/priv/static/static/js/29.a0eb0eee98462dc00d86.js differ diff --git a/priv/static/static/js/29.a0eb0eee98462dc00d86.js.map b/priv/static/static/js/29.a0eb0eee98462dc00d86.js.map new file mode 100644 index 000000000..357679d53 Binary files /dev/null and b/priv/static/static/js/29.a0eb0eee98462dc00d86.js.map differ diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js b/priv/static/static/js/3.44ee95fa34170fe38ef7.js similarity index 99% rename from priv/static/static/js/3.56898c1005d9ba1b8d4a.js rename to priv/static/static/js/3.44ee95fa34170fe38ef7.js index 6b20ecb04..ad2b9294c 100644 Binary files a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js and b/priv/static/static/js/3.44ee95fa34170fe38ef7.js differ diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map b/priv/static/static/js/3.44ee95fa34170fe38ef7.js.map similarity index 99% rename from priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map rename to priv/static/static/js/3.44ee95fa34170fe38ef7.js.map index 594d9047b..7efe5d6a5 100644 Binary files a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map and b/priv/static/static/js/3.44ee95fa34170fe38ef7.js.map differ diff --git a/priv/static/static/js/30.d78855ca19bf749be905.js b/priv/static/static/js/30.73f0507f6b66caa1b632.js similarity index 99% rename from priv/static/static/js/30.d78855ca19bf749be905.js rename to priv/static/static/js/30.73f0507f6b66caa1b632.js index 9202d19d3..0f1beeb58 100644 Binary files a/priv/static/static/js/30.d78855ca19bf749be905.js and b/priv/static/static/js/30.73f0507f6b66caa1b632.js differ diff --git a/priv/static/static/js/30.73f0507f6b66caa1b632.js.map b/priv/static/static/js/30.73f0507f6b66caa1b632.js.map new file mode 100644 index 000000000..e73f818cd Binary files /dev/null and b/priv/static/static/js/30.73f0507f6b66caa1b632.js.map differ diff --git a/priv/static/static/js/30.d78855ca19bf749be905.js.map b/priv/static/static/js/30.d78855ca19bf749be905.js.map deleted file mode 100644 index b9f39664d..000000000 Binary files a/priv/static/static/js/30.d78855ca19bf749be905.js.map and /dev/null differ diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js b/priv/static/static/js/4.77639012e321d98c064c.js similarity index 80% rename from priv/static/static/js/4.2d3bef896b463484e6eb.js rename to priv/static/static/js/4.77639012e321d98c064c.js index 4a611feb4..e8d35a81d 100644 Binary files a/priv/static/static/js/4.2d3bef896b463484e6eb.js and b/priv/static/static/js/4.77639012e321d98c064c.js differ diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js.map b/priv/static/static/js/4.77639012e321d98c064c.js.map similarity index 99% rename from priv/static/static/js/4.2d3bef896b463484e6eb.js.map rename to priv/static/static/js/4.77639012e321d98c064c.js.map index ebcc883e5..1a0373e08 100644 Binary files a/priv/static/static/js/4.2d3bef896b463484e6eb.js.map and b/priv/static/static/js/4.77639012e321d98c064c.js.map differ diff --git a/priv/static/static/js/5.84f3dce298bc720719c7.js b/priv/static/static/js/5.abcc811ac6e85e621b0d.js similarity index 98% rename from priv/static/static/js/5.84f3dce298bc720719c7.js rename to priv/static/static/js/5.abcc811ac6e85e621b0d.js index 242b2a525..1575d2a95 100644 Binary files a/priv/static/static/js/5.84f3dce298bc720719c7.js and b/priv/static/static/js/5.abcc811ac6e85e621b0d.js differ diff --git a/priv/static/static/js/5.84f3dce298bc720719c7.js.map b/priv/static/static/js/5.abcc811ac6e85e621b0d.js.map similarity index 57% rename from priv/static/static/js/5.84f3dce298bc720719c7.js.map rename to priv/static/static/js/5.abcc811ac6e85e621b0d.js.map index 4fcc32982..03251d1d8 100644 Binary files a/priv/static/static/js/5.84f3dce298bc720719c7.js.map and b/priv/static/static/js/5.abcc811ac6e85e621b0d.js.map differ diff --git a/priv/static/static/js/6.389907251866808cf2c4.js b/priv/static/static/js/6.389907251866808cf2c4.js new file mode 100644 index 000000000..def098eda Binary files /dev/null and b/priv/static/static/js/6.389907251866808cf2c4.js differ diff --git a/priv/static/static/js/6.b9497e1d403b901a664e.js.map b/priv/static/static/js/6.389907251866808cf2c4.js.map similarity index 57% rename from priv/static/static/js/6.b9497e1d403b901a664e.js.map rename to priv/static/static/js/6.389907251866808cf2c4.js.map index 5af0576c7..7b96d2998 100644 Binary files a/priv/static/static/js/6.b9497e1d403b901a664e.js.map and b/priv/static/static/js/6.389907251866808cf2c4.js.map differ diff --git a/priv/static/static/js/6.b9497e1d403b901a664e.js b/priv/static/static/js/6.b9497e1d403b901a664e.js deleted file mode 100644 index 8c66e9062..000000000 Binary files a/priv/static/static/js/6.b9497e1d403b901a664e.js and /dev/null differ diff --git a/priv/static/static/js/7.455b574116ce3f004ffb.js b/priv/static/static/js/7.33e3cc5c9abab3f21825.js similarity index 65% rename from priv/static/static/js/7.455b574116ce3f004ffb.js rename to priv/static/static/js/7.33e3cc5c9abab3f21825.js index 0e35f6904..6a4e332e9 100644 Binary files a/priv/static/static/js/7.455b574116ce3f004ffb.js and b/priv/static/static/js/7.33e3cc5c9abab3f21825.js differ diff --git a/priv/static/static/js/7.455b574116ce3f004ffb.js.map b/priv/static/static/js/7.33e3cc5c9abab3f21825.js.map similarity index 57% rename from priv/static/static/js/7.455b574116ce3f004ffb.js.map rename to priv/static/static/js/7.33e3cc5c9abab3f21825.js.map index 971b570e1..a04c36f4c 100644 Binary files a/priv/static/static/js/7.455b574116ce3f004ffb.js.map and b/priv/static/static/js/7.33e3cc5c9abab3f21825.js.map differ diff --git a/priv/static/static/js/8.5e0b07052c330e85bead.js b/priv/static/static/js/8.5e0b07052c330e85bead.js new file mode 100644 index 000000000..7fd0ec5a1 Binary files /dev/null and b/priv/static/static/js/8.5e0b07052c330e85bead.js differ diff --git a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map b/priv/static/static/js/8.5e0b07052c330e85bead.js.map similarity index 57% rename from priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map rename to priv/static/static/js/8.5e0b07052c330e85bead.js.map index e193375de..d324ed4b0 100644 Binary files a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map and b/priv/static/static/js/8.5e0b07052c330e85bead.js.map differ diff --git a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js b/priv/static/static/js/8.8db9f2dcc5ed429777f7.js deleted file mode 100644 index 79082fc7c..000000000 Binary files a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js and /dev/null differ diff --git a/priv/static/static/js/9.da3973d058660aa9612f.js b/priv/static/static/js/9.da3973d058660aa9612f.js deleted file mode 100644 index 50d977f67..000000000 Binary files a/priv/static/static/js/9.da3973d058660aa9612f.js and /dev/null differ diff --git a/priv/static/static/js/9.da3973d058660aa9612f.js.map b/priv/static/static/js/9.da3973d058660aa9612f.js.map deleted file mode 100644 index 4c8f70599..000000000 Binary files a/priv/static/static/js/9.da3973d058660aa9612f.js.map and /dev/null differ diff --git a/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js b/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js new file mode 100644 index 000000000..353737ab0 Binary files /dev/null and b/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js differ diff --git a/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js.map b/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js.map new file mode 100644 index 000000000..452afcc41 Binary files /dev/null and b/priv/static/static/js/9.f8e3aa590f4a66aedc3f.js.map differ diff --git a/priv/static/static/js/app.032cb80dafd1f208df1c.js b/priv/static/static/js/app.032cb80dafd1f208df1c.js new file mode 100644 index 000000000..c4b099811 Binary files /dev/null and b/priv/static/static/js/app.032cb80dafd1f208df1c.js differ diff --git a/priv/static/static/js/app.032cb80dafd1f208df1c.js.map b/priv/static/static/js/app.032cb80dafd1f208df1c.js.map new file mode 100644 index 000000000..397fbfbe8 Binary files /dev/null and b/priv/static/static/js/app.032cb80dafd1f208df1c.js.map differ diff --git a/priv/static/static/js/app.31bba9f1e242ff273dcb.js b/priv/static/static/js/app.31bba9f1e242ff273dcb.js deleted file mode 100644 index 22413689c..000000000 Binary files a/priv/static/static/js/app.31bba9f1e242ff273dcb.js and /dev/null differ diff --git a/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map b/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map deleted file mode 100644 index 8ff7a00c6..000000000 Binary files a/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js b/priv/static/static/js/vendors~app.811c8482146cad566f7e.js similarity index 92% rename from priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js rename to priv/static/static/js/vendors~app.811c8482146cad566f7e.js index 76c8a8dc1..c2114925d 100644 Binary files a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js and b/priv/static/static/js/vendors~app.811c8482146cad566f7e.js differ diff --git a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map b/priv/static/static/js/vendors~app.811c8482146cad566f7e.js.map similarity index 99% rename from priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map rename to priv/static/static/js/vendors~app.811c8482146cad566f7e.js.map index f3c067c15..858078059 100644 Binary files a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map and b/priv/static/static/js/vendors~app.811c8482146cad566f7e.js.map differ diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index 9b7d127fd..5aabeb744 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map index 5749809d5..20dac11d0 100644 Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs index d75c06cc7..f86d79826 100644 --- a/test/activity_expiration_test.exs +++ b/test/activity_expiration_test.exs @@ -11,7 +11,10 @@ defmodule Pleroma.ActivityExpirationTest do test "finds activities due to be deleted only" do activity = insert(:note_activity) - expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id}) + + expiration_due = + insert(:expiration_in_the_past, %{activity_id: activity.id}) |> Repo.preload(:activity) + activity2 = insert(:note_activity) insert(:expiration_in_the_future, %{activity_id: activity2.id}) diff --git a/test/config_test.exs b/test/config_test.exs index a46ab4302..1556e4237 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -28,6 +28,34 @@ test "get/1 with a list of keys" do assert Pleroma.Config.get([:azerty, :uiop], true) == true end + describe "nil values" do + setup do + Pleroma.Config.put(:lorem, nil) + Pleroma.Config.put(:ipsum, %{dolor: [sit: nil]}) + Pleroma.Config.put(:dolor, sit: %{amet: nil}) + + on_exit(fn -> Enum.each(~w(lorem ipsum dolor)a, &Pleroma.Config.delete/1) end) + end + + test "get/1 with an atom for nil value" do + assert Pleroma.Config.get(:lorem) == nil + end + + test "get/2 with an atom for nil value" do + assert Pleroma.Config.get(:lorem, true) == nil + end + + test "get/1 with a list of keys for nil value" do + assert Pleroma.Config.get([:ipsum, :dolor, :sit]) == nil + assert Pleroma.Config.get([:dolor, :sit, :amet]) == nil + end + + test "get/2 with a list of keys for nil value" do + assert Pleroma.Config.get([:ipsum, :dolor, :sit], true) == nil + assert Pleroma.Config.get([:dolor, :sit, :amet], true) == nil + end + end + test "get/1 when value is false" do Pleroma.Config.put([:instance, :false_test], false) Pleroma.Config.put([:instance, :nested], []) @@ -89,5 +117,23 @@ test "delete/2 with a list of keys" do Pleroma.Config.put([:delete_me, :delete_me], hello: "world", world: "Hello") Pleroma.Config.delete([:delete_me, :delete_me, :world]) assert Pleroma.Config.get([:delete_me, :delete_me]) == [hello: "world"] + + assert Pleroma.Config.delete([:this_key_does_not_exist]) + assert Pleroma.Config.delete([:non, :existing, :key]) + end + + test "fetch/1" do + Pleroma.Config.put([:lorem], :ipsum) + Pleroma.Config.put([:ipsum], dolor: :sit) + + assert Pleroma.Config.fetch([:lorem]) == {:ok, :ipsum} + assert Pleroma.Config.fetch(:lorem) == {:ok, :ipsum} + assert Pleroma.Config.fetch([:ipsum, :dolor]) == {:ok, :sit} + assert Pleroma.Config.fetch([:lorem, :ipsum]) == :error + assert Pleroma.Config.fetch([:loremipsum]) == :error + assert Pleroma.Config.fetch(:loremipsum) == :error + + Pleroma.Config.delete([:lorem]) + Pleroma.Config.delete([:ipsum]) end end diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs index e6e34cba8..3da45056b 100644 --- a/test/emails/mailer_test.exs +++ b/test/emails/mailer_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Emails.MailerTest do test "not send email when mailer is disabled" do Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) Mailer.deliver(@email) + :timer.sleep(100) refute_email_sent( from: {"Pleroma", "noreply@example.com"}, @@ -30,6 +31,7 @@ test "not send email when mailer is disabled" do test "send email" do Mailer.deliver(@email) + :timer.sleep(100) assert_email_sent( from: {"Pleroma", "noreply@example.com"}, @@ -41,6 +43,7 @@ test "send email" do test "perform" do Mailer.perform(:deliver_async, @email, []) + :timer.sleep(100) assert_email_sent( from: {"Pleroma", "noreply@example.com"}, diff --git a/test/fixtures/mastodon-question-activity.json b/test/fixtures/mastodon-question-activity.json index ac329c7d5..3648b9f90 100644 --- a/test/fixtures/mastodon-question-activity.json +++ b/test/fixtures/mastodon-question-activity.json @@ -49,7 +49,6 @@ "en": "

Why is Tenshi eating a corndog so cute?

" }, "endTime": "2019-05-11T09:03:36Z", - "closed": "2019-05-11T09:03:36Z", "attachment": [], "tag": [], "replies": { diff --git a/test/fixtures/tesla_mock/poll_attachment.json b/test/fixtures/tesla_mock/poll_attachment.json new file mode 100644 index 000000000..92e822dc8 --- /dev/null +++ b/test/fixtures/tesla_mock/poll_attachment.json @@ -0,0 +1,99 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://patch.cx/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://patch.cx/users/rin", + "anyOf": [], + "attachment": [ + { + "mediaType": "image/jpeg", + "name": "screenshot_mpv:Totoro@01:18:44.345.jpg", + "type": "Document", + "url": "https://shitposter.club/media/3bb4c4d402f8fdcc7f80963c3d7cf6f10f936897fd374922ade33199d2f86d87.jpg?name=screenshot_mpv%3ATotoro%4001%3A18%3A44.345.jpg" + } + ], + "attributedTo": "https://patch.cx/users/rin", + "cc": [ + "https://patch.cx/users/rin/followers" + ], + "closed": "2020-06-19T23:22:02.754678Z", + "content": "@rinpatch", + "closed": "2019-09-19T00:32:36.785333", + "content": "can you vote on this poll?", + "id": "https://patch.cx/objects/tesla_mock/poll_attachment", + "oneOf": [ + { + "name": "a", + "replies": { + "totalItems": 0, + "type": "Collection" + }, + "type": "Note" + }, + { + "name": "A", + "replies": { + "totalItems": 0, + "type": "Collection" + }, + "type": "Note" + }, + { + "name": "Aa", + "replies": { + "totalItems": 0, + "type": "Collection" + }, + "type": "Note" + }, + { + "name": "AA", + "replies": { + "totalItems": 0, + "type": "Collection" + }, + "type": "Note" + }, + { + "name": "AAa", + "replies": { + "totalItems": 1, + "type": "Collection" + }, + "type": "Note" + }, + { + "name": "AAA", + "replies": { + "totalItems": 3, + "type": "Collection" + }, + "type": "Note" + } + ], + "published": "2020-06-19T23:12:02.786113Z", + "sensitive": false, + "summary": "", + "tag": [ + { + "href": "https://mastodon.sdf.org/users/rinpatch", + "name": "@rinpatch@mastodon.sdf.org", + "type": "Mention" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://mastodon.sdf.org/users/rinpatch" + ], + "type": "Question", + "voters": [ + "https://shitposter.club/users/moonman", + "https://skippers-bin.com/users/7v1w1r8ce6", + "https://mastodon.sdf.org/users/rinpatch", + "https://mastodon.social/users/emelie" + ] +} diff --git a/test/migrations/20200802170532_fix_legacy_tags_test.exs b/test/migrations/20200802170532_fix_legacy_tags_test.exs new file mode 100644 index 000000000..3b4dee407 --- /dev/null +++ b/test/migrations/20200802170532_fix_legacy_tags_test.exs @@ -0,0 +1,24 @@ +defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do + alias Pleroma.User + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + + setup_all do: require_migration("20200802170532_fix_legacy_tags") + + test "change/0 converts legacy user tags into correct values", %{migration: migration} do + user = insert(:user, tags: ["force_nsfw", "force_unlisted", "verified"]) + user2 = insert(:user) + + assert :ok == migration.change() + + fixed_user = User.get_by_id(user.id) + fixed_user2 = User.get_by_id(user2.id) + + assert fixed_user.tags == ["mrf_tag:media-force-nsfw", "mrf_tag:force-unlisted", "verified"] + assert fixed_user2.tags == [] + + # user2 should not have been updated + assert fixed_user2.updated_at == fixed_user2.inserted_at + end +end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index d9098ea1b..16cfa7f5c 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -177,6 +177,13 @@ test "handle HTTP 404 response" do "https://mastodon.example.org/users/userisgone404" ) end + + test "it can fetch pleroma polls with attachments" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment") + + assert object + end end describe "pruning" do diff --git a/test/report_note_test.exs b/test/report_note_test.exs new file mode 100644 index 000000000..25c1d6a61 --- /dev/null +++ b/test/report_note_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReportNoteTest do + alias Pleroma.ReportNote + use Pleroma.DataCase + import Pleroma.Factory + + test "create/3" do + user = insert(:user) + report = insert(:report_activity) + assert {:ok, note} = ReportNote.create(user.id, report.id, "naughty boy") + assert note.content == "naughty boy" + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index b23918dd1..7ef681258 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -56,6 +56,13 @@ defp request_content_type(%{conn: conn}) do [conn: conn] end + defp empty_json_response(conn) do + body = response(conn, 204) + response_content_type(conn, :json) + + body + end + defp json_response_and_validate_schema( %{ private: %{ @@ -79,7 +86,7 @@ defp json_response_and_validate_schema( end schema = lookup[op_id].responses[status].content[content_type].schema - json = json_response(conn, status) + json = if status == 204, do: empty_json_response(conn), else: json_response(conn, status) case OpenApiSpex.cast_value(json, schema, spec) do {:ok, _data} -> diff --git a/test/support/factory.ex b/test/support/factory.ex index 635d83650..486eda8da 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -297,6 +297,30 @@ def follow_activity_factory do } end + def report_activity_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user) + activity = attrs[:activity] || insert(:note_activity) + state = attrs[:state] || "open" + + data = %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => user.ap_id, + "type" => "Flag", + "object" => [activity.actor, activity.data["id"]], + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "to" => [], + "cc" => [activity.actor], + "context" => activity.data["context"], + "state" => state + } + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] ++ data["cc"] + } + end + def oauth_app_factory do %Pleroma.Web.OAuth.App{ client_name: sequence(:client_name, &"Some client #{&1}"), diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 5cbf2e291..ecd4b1e18 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -17,9 +17,19 @@ defmacro clear_config(config_path) do defmacro clear_config(config_path, do: yield) do quote do - initial_setting = Config.get(unquote(config_path)) + initial_setting = Config.fetch(unquote(config_path)) unquote(yield) - on_exit(fn -> Config.put(unquote(config_path), initial_setting) end) + + on_exit(fn -> + case initial_setting do + :error -> + Config.delete(unquote(config_path)) + + {:ok, value} -> + Config.put(unquote(config_path), value) + end + end) + :ok end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 19a202654..eeeba7880 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -82,6 +82,14 @@ def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do }} end + def get("https://patch.cx/objects/tesla_mock/poll_attachment", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_attachment.json") + }} + end + def get( "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie", _, diff --git a/test/tasks/app_test.exs b/test/tasks/app_test.exs index b8f03566d..71a84ac8e 100644 --- a/test/tasks/app_test.exs +++ b/test/tasks/app_test.exs @@ -50,13 +50,13 @@ test "with errors" do defp assert_app(name, redirect, scopes) do app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name) - assert_received {:mix_shell, :info, [message]} + assert_receive {:mix_shell, :info, [message]} assert message == "#{name} successfully created:" - assert_received {:mix_shell, :info, [message]} + assert_receive {:mix_shell, :info, [message]} assert message == "App client_id: #{app.client_id}" - assert_received {:mix_shell, :info, [message]} + assert_receive {:mix_shell, :info, [message]} assert message == "App client_secret: #{app.client_secret}" assert app.scopes == scopes diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs index 883828d77..3a28aa133 100644 --- a/test/tasks/database_test.exs +++ b/test/tasks/database_test.exs @@ -127,4 +127,43 @@ test "it turns OrderedCollection likes into empty arrays" do assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) end end + + describe "ensure_expiration" do + test "it adds to expiration old statuses" do + %{id: activity_id1} = insert(:note_activity) + + %{id: activity_id2} = + insert(:note_activity, %{inserted_at: NaiveDateTime.from_iso8601!("2015-01-23 23:50:07")}) + + %{id: activity_id3} = activity3 = insert(:note_activity) + + expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(60 * 61, :second) + |> NaiveDateTime.truncate(:second) + + Pleroma.ActivityExpiration.create(activity3, expires_at) + + Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) + + expirations = + Pleroma.ActivityExpiration + |> order_by(:activity_id) + |> Repo.all() + + assert [ + %Pleroma.ActivityExpiration{ + activity_id: ^activity_id1 + }, + %Pleroma.ActivityExpiration{ + activity_id: ^activity_id2, + scheduled_at: ~N[2016-01-23 23:50:07] + }, + %Pleroma.ActivityExpiration{ + activity_id: ^activity_id3, + scheduled_at: ^expires_at + } + ] = expirations + end + end end diff --git a/test/tasks/release_env_test.exs b/test/tasks/release_env_test.exs deleted file mode 100644 index 519f1eba9..000000000 --- a/test/tasks/release_env_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.ReleaseEnvTest do - use ExUnit.Case - import ExUnit.CaptureIO, only: [capture_io: 1] - - @path "config/pleroma.test.env" - - def do_clean do - if File.exists?(@path) do - File.rm_rf(@path) - end - end - - setup do - do_clean() - on_exit(fn -> do_clean() end) - :ok - end - - test "generate pleroma.env" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.ReleaseEnv.run(["gen", "--path", @path, "--force"]) - end) =~ "The file generated" - - assert File.read!(@path) =~ "RELEASE_COOKIE=" - end -end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs index a1b7e46cd..8ed7d650b 100644 --- a/test/upload/filter/exiftool_test.exs +++ b/test/upload/filter/exiftool_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Upload.Filter.ExiftoolTest do alias Pleroma.Upload.Filter test "apply exiftool filter" do + assert Pleroma.Utils.command_available?("exiftool") + File.cp!( "test/fixtures/DSCN0010.jpg", "test/fixtures/DSCN0010_tmp.jpg" diff --git a/test/user/welcome_chat_massage_test.exs b/test/user/welcome_chat_massage_test.exs new file mode 100644 index 000000000..fe26d6e4d --- /dev/null +++ b/test/user/welcome_chat_massage_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeChatMessageTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User.WelcomeChatMessage + + import Pleroma.Factory + + setup do: clear_config([:welcome]) + + describe "post_message/1" do + test "send a chat welcome message" do + welcome_user = insert(:user, name: "mewmew") + user = insert(:user) + + Config.put([:welcome, :chat_message, :enabled], true) + Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + + Config.put( + [:welcome, :chat_message, :message], + "Hello, welcome to Blob/Cat!" + ) + + {:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user) + + assert user.ap_id in activity.recipients + assert Pleroma.Object.normalize(activity).data["type"] == "ChatMessage" + assert Pleroma.Object.normalize(activity).data["content"] == "Hello, welcome to Blob/Cat!" + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index 904cea536..b47405895 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -412,8 +412,36 @@ test "it sends a welcome message if it is set" do welcome_user = insert(:user) Pleroma.Config.put([:welcome, :direct_message, :enabled], true) Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a cool site") + Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message") + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "direct message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome chat message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome email message if it is set" do + welcome_user = insert(:user) Pleroma.Config.put([:welcome, :email, :enabled], true) Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email) @@ -428,11 +456,6 @@ test "it sends a welcome message if it is set" do {:ok, registered_user} = User.register(cng) ObanHelpers.perform_all() - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "cool site" - assert activity.actor == welcome_user.ap_id - assert_email_sent( from: {instance_name, welcome_user.email}, to: {registered_user.name, registered_user.email}, @@ -490,6 +513,29 @@ test "it restricts certain nicknames" do refute changeset.valid? end + test "it blocks blacklisted email domains" do + clear_config([User, :email_blacklist], ["trolling.world"]) + + # Block with match + params = Map.put(@full_user_data, :email, "troll@trolling.world") + changeset = User.register_changeset(%User{}, params) + refute changeset.valid? + + # Block with subdomain match + params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world") + changeset = User.register_changeset(%User{}, params) + refute changeset.valid? + + # Pass with different domains that are similar + params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + + params = Map.put(@full_user_data, :email, "troll@trolling.world.us") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + end + test "it sets the password_hash and ap_id" do changeset = User.register_changeset(%User{}, @full_user_data) diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs index 8babf49e7..f25cf8b12 100644 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -7,11 +7,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" + @local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe" test "adds `expires_at` property" do assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = ActivityExpirationPolicy.filter(%{ "id" => @id, + "actor" => @local_actor, "type" => "Create", "object" => %{"type" => "Note"} }) @@ -25,6 +27,7 @@ test "keeps existing `expires_at` if it less than the config setting" do assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = ActivityExpirationPolicy.filter(%{ "id" => @id, + "actor" => @local_actor, "type" => "Create", "expires_at" => expires_at, "object" => %{"type" => "Note"} @@ -37,6 +40,7 @@ test "overwrites existing `expires_at` if it greater than the config setting" do assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = ActivityExpirationPolicy.filter(%{ "id" => @id, + "actor" => @local_actor, "type" => "Create", "expires_at" => too_distant_future, "object" => %{"type" => "Note"} @@ -49,6 +53,7 @@ test "ignores remote activities" do assert {:ok, activity} = ActivityExpirationPolicy.filter(%{ "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", "type" => "Create", "object" => %{"type" => "Note"} }) @@ -60,6 +65,7 @@ test "ignores non-Create/Note activities" do assert {:ok, activity} = ActivityExpirationPolicy.filter(%{ "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", "type" => "Follow" }) @@ -68,6 +74,7 @@ test "ignores non-Create/Note activities" do assert {:ok, activity} = ActivityExpirationPolicy.filter(%{ "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", "type" => "Create", "object" => %{"type" => "Cofe"} }) diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs index b0fb753bd..cf6acc9a2 100644 --- a/test/web/activity_pub/mrf/object_age_policy_test.exs +++ b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -38,6 +38,17 @@ defp get_new_message do end describe "with reject action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + assert match?({:reject, _}, ObjectAgePolicy.filter(data)) + end + test "it rejects an old post" do Config.put([:mrf_object_age, :actions], [:reject]) @@ -56,6 +67,21 @@ test "it allows a new post" do end describe "with delist action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + test "it delists an old post" do Config.put([:mrf_object_age, :actions], [:delist]) @@ -80,6 +106,22 @@ test "it allows a new post" do end describe "with strip_followers action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + test "it strips followers collections from an old post" do Config.put([:mrf_object_age, :actions], [:strip_followers]) diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index e842d8d8d..d7dde62c4 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do import Pleroma.Factory alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF.SimplePolicy + alias Pleroma.Web.CommonAPI setup do: clear_config(:mrf_simple, @@ -15,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do federated_timeline_removal: [], report_removal: [], reject: [], + followers_only: [], accept: [], avatar_removal: [], banner_removal: [], @@ -261,6 +263,64 @@ test "actor has a matching host" do end end + describe "when :followers_only" do + test "is empty" do + Config.put([:mrf_simple, :followers_only], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + actor = insert(:user) + following_user = insert(:user) + non_following_user = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(following_user, actor) + + activity = %{ + "actor" => actor.ap_id, + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + } + + dm_activity = %{ + "actor" => actor.ap_id, + "to" => [ + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [] + } + + actor_domain = + activity + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :followers_only], [actor_domain]) + + assert {:ok, new_activity} = SimplePolicy.filter(activity) + assert actor.follower_address in new_activity["cc"] + assert following_user.ap_id in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"] + refute non_following_user.ap_id in new_activity["to"] + refute non_following_user.ap_id in new_activity["cc"] + + assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity) + assert new_dm_activity["to"] == [following_user.ap_id] + assert new_dm_activity["cc"] == [] + end + end + describe "when :accept" do test "is empty" do Config.put([:mrf_simple, :accept], []) diff --git a/test/web/activity_pub/object_validators/accept_validation_test.exs b/test/web/activity_pub/object_validators/accept_validation_test.exs new file mode 100644 index 000000000..d6111ba41 --- /dev/null +++ b/test/web/activity_pub/object_validators/accept_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, accept_data, _} = Builder.accept(followed, follow_activity) + + %{accept_data: accept_data, followed: followed} + end + + test "it validates a basic 'accept'", %{accept_data: accept_data} do + assert {:ok, _, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the actor doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the accepted activity doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "for an accepted follow, it only validates if the actor of the accept is the followed actor", + %{accept_data: accept_data} do + stranger = insert(:user) + + accept_data = + accept_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end +end diff --git a/test/web/activity_pub/object_validators/delete_validation_test.exs b/test/web/activity_pub/object_validators/delete_validation_test.exs index 42cd18298..02683b899 100644 --- a/test/web/activity_pub/object_validators/delete_validation_test.exs +++ b/test/web/activity_pub/object_validators/delete_validation_test.exs @@ -87,7 +87,7 @@ test "it's invalid if the actor of the object and the actor of delete are from d {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) - assert {:actor, {"is not allowed to delete object", []}} in cng.errors + assert {:actor, {"is not allowed to modify object", []}} in cng.errors end test "it's valid if the actor of the object is a local superuser", diff --git a/test/web/activity_pub/object_validators/reject_validation_test.exs b/test/web/activity_pub/object_validators/reject_validation_test.exs new file mode 100644 index 000000000..370bb6e5c --- /dev/null +++ b/test/web/activity_pub/object_validators/reject_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.RejectValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, reject_data, _} = Builder.reject(followed, follow_activity) + + %{reject_data: reject_data, followed: followed} + end + + test "it validates a basic 'reject'", %{reject_data: reject_data} do + assert {:ok, _, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the actor doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the rejected activity doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "for an rejected follow, it only validates if the actor of the reject is the followed actor", + %{reject_data: reject_data} do + stranger = insert(:user) + + reject_data = + reject_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 4a08eb7ee..9efbaad04 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -19,8 +19,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.CommonAPI - import Pleroma.Factory + import ExUnit.CaptureLog import Mock + import Pleroma.Factory describe "handle_after_transaction" do test "it streams out notifications and streams" do @@ -221,6 +222,22 @@ test "it handles user deletions", %{delete_user: delete, user: user} do assert User.get_cached_by_ap_id(user.ap_id).deactivated end + + test "it logs issues with objects deletion", %{ + delete: delete, + object: object + } do + {:ok, object} = + object + |> Object.change(%{data: Map.delete(object.data, "actor")}) + |> Repo.update() + + Object.invalid_object_cache(object) + + assert capture_log(fn -> + {:error, :no_object_actor} = SideEffects.handle(delete) + end) =~ "object doesn't have an actor" + end end describe "EmojiReact objects" do diff --git a/test/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/web/activity_pub/transmogrifier/accept_handling_test.exs new file mode 100644 index 000000000..77d468f5c --- /dev/null +++ b/test/web/activity_pub/transmogrifier/accept_handling_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do + use Pleroma.DataCase + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming accepts which were pre-accepted" do + follower = insert(:user) + followed = insert(:user) + + {:ok, follower} = User.follow(follower, followed) + assert User.following?(follower, followed) == true + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + object = + accept_data["object"] + |> Map.put("actor", follower.ap_id) + |> Map.put("id", follow_activity.data["id"]) + + accept_data = Map.put(accept_data, "object", object) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + refute activity.local + + assert activity.data["object"] == follow_activity.data["id"] + + assert activity.data["id"] == accept_data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + end + + test "it works for incoming accepts which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + assert activity.data["object"] == follow_activity.data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + + follower = User.get_by_id(follower.id) + assert follower.following_count == 1 + + followed = User.get_by_id(followed.id) + assert followed.follower_count == 1 + end + + test "it fails for incoming accepts which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end +end diff --git a/test/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/web/activity_pub/transmogrifier/answer_handling_test.exs new file mode 100644 index 000000000..0f6605c3f --- /dev/null +++ b/test/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "incoming, rewrites Note to Answer and increments vote counters" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + object = Object.normalize(activity) + + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + answer_object = Object.normalize(activity) + assert answer_object.data["type"] == "Answer" + assert answer_object.data["inReplyTo"] == object.data["id"] + + new_object = Object.get_by_ap_id(object.data["id"]) + assert new_object.data["replies_count"] == object.data["replies_count"] + + assert Enum.any?( + new_object.data["oneOf"], + fn + %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true + _ -> false + end + ) + end + + test "outgoing, rewrites Answer to Note" do + user = insert(:user) + + {:ok, poll_activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + poll_object = Object.normalize(poll_activity) + # TODO: Replace with CommonAPI vote creation when implemented + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + assert data["object"]["type"] == "Note" + end +end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index d6736dc3e..31274c067 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -124,6 +124,24 @@ test "it fetches the actor if they aren't in our system" do {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) end + test "it doesn't work for deactivated users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, + ap_id: data["actor"], + local: false, + last_refreshed_at: DateTime.utc_now(), + deactivated: true + ) + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + test "it inserts it and creates a chat" do data = File.read!("test/fixtures/create-chat-message.json") diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 17e764ca1..757d90941 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -185,5 +185,24 @@ test "it works for incoming follow requests from hubzilla" do assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) end + + test "it works for incoming follows to locked account" do + pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Follow" + assert data["object"] == user.ap_id + assert data["state"] == "pending" + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert [^pending_follower] = User.get_follow_requests(user) + end end end diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs new file mode 100644 index 000000000..9fb965d7f --- /dev/null +++ b/test/web/activity_pub/transmogrifier/question_handling_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Mastodon Question activity" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity, false) + + assert object.data["closed"] == "2019-05-11T09:03:36Z" + + assert object.data["context"] == activity.data["context"] + + assert object.data["context"] == + "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation" + + assert object.data["context_id"] + + assert object.data["anyOf"] == [] + + assert Enum.sort(object.data["oneOf"]) == + Enum.sort([ + %{ + "name" => "25 char limit is dumb", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Dunno", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Everyone knows that!", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "I can't even fit a funny", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + } + ]) + + user = insert(:user) + + {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id}) + + reply_object = Object.normalize(reply_activity, false) + + assert reply_object.data["context"] == object.data["context"] + assert reply_object.data["context_id"] == object.data["context_id"] + end + + test "Mastodon Question activity with HTML tags in plaintext" do + options = [ + %{ + "type" => "Note", + "name" => "", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) + end + + test "returns an error if received a second time" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert {:error, {:validate_object, {:error, _}}} = Transmogrifier.handle_incoming(data) + end + + test "accepts a Question with no content" do + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "content"], "") + + assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/web/activity_pub/transmogrifier/reject_handling_test.exs new file mode 100644 index 000000000..7592fbe1c --- /dev/null +++ b/test/web/activity_pub/transmogrifier/reject_handling_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it fails for incoming rejects which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end + + test "it works for incoming rejects which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, follower} = User.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) == true + + reject_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == false + end + + test "it rejects activities without a valid ID" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + |> Map.put("id", "") + + :error = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/web/activity_pub/transmogrifier/undo_handling_test.exs index 01dd6c370..8683f7135 100644 --- a/test/web/activity_pub/transmogrifier/undo_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -130,7 +130,7 @@ test "it works for incoming unannounces with an existing notice" do "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" end - test "it works for incomming unfollows with an existing follow" do + test "it works for incoming unfollows with an existing follow" do user = insert(:user) follow_data = diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 7d33feaf2..0dd4e6e47 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -163,6 +163,14 @@ test "it does not crash if the object in inReplyTo can't be fetched" do end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil" end + test "it does not work for deactivated users" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + insert(:user, ap_id: data["actor"], deactivated: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + test "it works for incoming notices" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() @@ -217,23 +225,6 @@ test "it works for incoming notices with hashtags" do assert Enum.at(object.data["tag"], 2) == "moo" end - test "it works for incoming questions" do - data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(activity) - - assert Enum.all?(object.data["oneOf"], fn choice -> - choice["name"] in [ - "Dunno", - "Everyone knows that!", - "25 char limit is dumb", - "I can't even fit a funny" - ] - end) - end - test "it works for incoming listens" do data = %{ "@context" => "https://www.w3.org/ns/activitystreams", @@ -263,38 +254,6 @@ test "it works for incoming listens" do assert object.data["length"] == 180_000 end - test "it rewrites Note votes to Answers and increments vote counters on question activities" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - object = Object.normalize(activity) - - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - answer_object = Object.normalize(activity) - assert answer_object.data["type"] == "Answer" - object = Object.get_by_ap_id(object.data["id"]) - - assert Enum.any?( - object.data["oneOf"], - fn - %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true - _ -> false - end - ) - end - test "it works for incoming notices with contentMap" do data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() @@ -400,7 +359,7 @@ test "it strips internal reactions" do refute Map.has_key?(object_data, "reaction_count") end - test "it works for incomming unfollows with an existing follow" do + test "it works for incoming unfollows with an existing follow" do user = insert(:user) follow_data = @@ -425,204 +384,6 @@ test "it works for incomming unfollows with an existing follow" do refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) end - test "it works for incoming follows to locked account" do - pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - user = insert(:user, locked: true) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Follow" - assert data["object"] == user.ap_id - assert data["state"] == "pending" - assert data["actor"] == "http://mastodon.example.org/users/admin" - - assert [^pending_follower] = User.get_follow_requests(user) - end - - test "it works for incoming accepts which were pre-accepted" do - follower = insert(:user) - followed = insert(:user) - - {:ok, follower} = User.follow(follower, followed) - assert User.following?(follower, followed) == true - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - object = - accept_data["object"] - |> Map.put("actor", follower.ap_id) - |> Map.put("id", follow_activity.data["id"]) - - accept_data = Map.put(accept_data, "object", object) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - refute activity.local - - assert activity.data["object"] == follow_activity.data["id"] - - assert activity.data["id"] == accept_data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - end - - test "it works for incoming accepts which were orphaned" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - assert activity.data["object"] == follow_activity.data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - end - - test "it works for incoming accepts which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - assert activity.data["object"] == follow_activity.data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - - follower = User.get_by_id(follower.id) - assert follower.following_count == 1 - - followed = User.get_by_id(followed.id) - assert followed.follower_count == 1 - end - - test "it fails for incoming accepts which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - :error = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end - - test "it fails for incoming rejects which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - :error = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end - - test "it works for incoming rejects which are orphaned" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, follower} = User.follow(follower, followed) - {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) == true - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - reject_data = - Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id)) - - {:ok, activity} = Transmogrifier.handle_incoming(reject_data) - refute activity.local - assert activity.data["id"] == reject_data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == false - end - - test "it works for incoming rejects which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, follower} = User.follow(follower, followed) - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) == true - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == false - end - - test "it rejects activities without a valid ID" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - |> Map.put("id", "") - - :error = Transmogrifier.handle_incoming(data) - end - test "skip converting the content when it is nil" do object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe" @@ -669,7 +430,8 @@ test "it remaps video URLs as attachments if necessary" do %{ "href" => "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4" + "mediaType" => "video/mp4", + "type" => "Link" } ] } @@ -688,7 +450,8 @@ test "it remaps video URLs as attachments if necessary" do %{ "href" => "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", - "mediaType" => "video/mp4" + "mediaType" => "video/mp4", + "type" => "Link" } ] } @@ -1261,30 +1024,6 @@ test "successfully reserializes a message with AS2 objects in IR" do end end - test "Rewrites Answers to Notes" do - user = insert(:user) - - {:ok, poll_activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - poll_object = Object.normalize(poll_activity) - # TODO: Replace with CommonAPI vote creation when implemented - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - - assert data["object"]["type"] == "Note" - end - describe "fix_explicit_addressing" do setup do user = insert(:user) @@ -1532,8 +1271,13 @@ test "returns modified object when attachment is map" do "attachment" => [ %{ "mediaType" => "video/mp4", + "type" => "Document", "url" => [ - %{"href" => "https://peertube.moe/stat-480.mp4", "mediaType" => "video/mp4"} + %{ + "href" => "https://peertube.moe/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } ] } ] @@ -1550,14 +1294,24 @@ test "returns modified object when attachment is list" do "attachment" => [ %{ "mediaType" => "video/mp4", + "type" => "Document", "url" => [ - %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"} + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } ] }, %{ "mediaType" => "video/mp4", + "type" => "Document", "url" => [ - %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"} + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } ] } ] diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index b5d5bd8c7..66d4b1ef3 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -158,6 +158,8 @@ test "single user", %{admin: admin, conn: conn} do user = insert(:user) clear_config([:instance, :federating], true) + refute user.deactivated + with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do conn = @@ -176,6 +178,9 @@ test "single user", %{admin: admin, conn: conn} do assert json_response(conn, 200) == [user.nickname] + user = Repo.get(User, user.id) + assert user.deactivated + assert called(Pleroma.Web.Federator.publish(:_)) end end @@ -439,7 +444,7 @@ test "it appends specified tags to users with specified nicknames", %{ user1: user1, user2: user2 } do - assert json_response(conn, :no_content) + assert empty_json_response(conn) assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] @@ -457,7 +462,7 @@ test "it appends specified tags to users with specified nicknames", %{ end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert json_response(conn, :no_content) + assert empty_json_response(conn) assert User.get_cached_by_id(user3.id).tags == ["unchanged"] end end @@ -485,7 +490,7 @@ test "it removes specified tags from users with specified nicknames", %{ user1: user1, user2: user2 } do - assert json_response(conn, :no_content) + assert empty_json_response(conn) assert User.get_cached_by_id(user1.id).tags == [] assert User.get_cached_by_id(user2.id).tags == ["y"] @@ -503,7 +508,7 @@ test "it removes specified tags from users with specified nicknames", %{ end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert json_response(conn, :no_content) + assert empty_json_response(conn) assert User.get_cached_by_id(user3.id).tags == ["unchanged"] end end @@ -1164,6 +1169,27 @@ test "load users with tags list", %{conn: conn} do } end + test "`active` filters out users pending approval", %{token: token} do + insert(:user, approval_pending: true) + %{id: user_id} = insert(:user, approval_pending: false) + %{id: admin_id} = token.user + + conn = + build_conn() + |> assign(:user, token.user) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?filters=active") + + assert %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{"id" => ^admin_id}, + %{"id" => ^user_id} + ] + } = json_response(conn, 200) + end + test "it works with multiple filters" do admin = insert(:user, nickname: "john", is_admin: true) token = insert(:oauth_admin_token, user: admin) @@ -1756,7 +1782,7 @@ test "sets password_reset_pending to true", %{conn: conn} do conn = patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) - assert json_response(conn, 204) == "" + assert empty_json_response(conn) == "" ObanHelpers.perform_all() diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs index 61bc9fd39..4e897455f 100644 --- a/test/web/admin_api/controllers/config_controller_test.exs +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -1342,6 +1342,75 @@ test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] ] end + + test "enables the welcome messages", %{conn: conn} do + clear_config([:welcome]) + + params = %{ + "group" => ":pleroma", + "key" => ":welcome", + "value" => [ + %{ + "tuple" => [ + ":direct_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":chat_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":email", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":sender", %{"tuple" => ["pleroma@dev.dev", "Pleroma"]}]}, + %{"tuple" => [":subject", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":html", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":text", "Welcome to <%= instance_name %>!"]} + ] + ] + } + ] + } + + refute Pleroma.User.WelcomeEmail.enabled?() + refute Pleroma.User.WelcomeMessage.enabled?() + refute Pleroma.User.WelcomeChatMessage.enabled?() + + res = + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => [params]}) + |> json_response_and_validate_schema(200) + + assert Pleroma.User.WelcomeEmail.enabled?() + assert Pleroma.User.WelcomeMessage.enabled?() + assert Pleroma.User.WelcomeChatMessage.enabled?() + + assert res == %{ + "configs" => [ + %{ + "db" => [":direct_message", ":chat_message", ":email"], + "group" => ":pleroma", + "key" => ":welcome", + "value" => params["value"] + } + ], + "need_reboot" => false + } + end end describe "GET /api/pleroma/admin/config/descriptions" do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 313dda21b..4ba6232dc 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -458,6 +458,11 @@ test "it adds emoji in the object" do end describe "posting" do + test "deactivated users can't post" do + user = insert(:user, deactivated: true) + assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) + end + test "it supports explicit addressing" do user = insert(:user) user_two = insert(:user) diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index b888e4c71..2e6704726 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -214,6 +214,10 @@ test "updates the user's name", %{conn: conn} do assert user_data = json_response_and_validate_schema(conn, 200) assert user_data["display_name"] == "markorepairs" + + update_activity = Repo.one(Pleroma.Activity) + assert update_activity.data["type"] == "Update" + assert update_activity.data["object"]["name"] == "markorepairs" end test "updates the user's avatar", %{user: user, conn: conn} do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index d390c3ce1..17a1e7d66 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -940,17 +940,32 @@ test "registers and logs in without :account_activation_required / :account_appr assert refresh assert scope == "read write follow" + clear_config([User, :email_blacklist], ["example.org"]) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + } + conn = build_conn() |> put_req_header("content-type", "multipart/form-data") |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - }) + |> post("/api/v1/accounts", params) + + assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = + json_response_and_validate_schema(conn, 400) + + Pleroma.Config.put([User, :email_blacklist], []) + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) %{ "access_token" => token, diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs index f29547d13..0d426ec34 100644 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -64,11 +64,13 @@ test "fetching a list of filters" do test "get a filter" do %{user: user, conn: conn} = oauth_access(["read:filters"]) + # check whole_word false query = %Pleroma.Filter{ user_id: user.id, filter_id: 2, phrase: "knight", - context: ["home"] + context: ["home"], + whole_word: false } {:ok, filter} = Pleroma.Filter.create(query) @@ -76,6 +78,25 @@ test "get a filter" do conn = get(conn, "/api/v1/filters/#{filter.filter_id}") assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == false + + # check whole_word true + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 3, + phrase: "knight", + context: ["home"], + whole_word: true + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") + + assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == true end test "update a filter" do @@ -86,7 +107,8 @@ test "update a filter" do filter_id: 2, phrase: "knight", context: ["home"], - hide: true + hide: true, + whole_word: true } {:ok, _filter} = Pleroma.Filter.create(query) @@ -108,6 +130,7 @@ test "update a filter" do assert response["phrase"] == new.phrase assert response["context"] == new.context assert response["irreversible"] == true + assert response["whole_word"] == true end test "delete a filter" do diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs index c08be37d4..0c5a38bf6 100644 --- a/test/web/mastodon_api/mastodon_api_test.exs +++ b/test/web/mastodon_api/mastodon_api_test.exs @@ -17,8 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do test "returns error when followed user is deactivated" do follower = insert(:user) user = insert(:user, local: true, deactivated: true) - {:error, error} = MastodonAPI.follow(follower, user) - assert error == :rejected + assert {:error, _error} = MastodonAPI.follow(follower, user) end test "following for user" do diff --git a/test/web/mastodon_api/views/poll_view_test.exs b/test/web/mastodon_api/views/poll_view_test.exs index 76672f36c..b7e2f17ef 100644 --- a/test/web/mastodon_api/views/poll_view_test.exs +++ b/test/web/mastodon_api/views/poll_view_test.exs @@ -135,4 +135,33 @@ test "does not crash on polls with no end date" do assert result[:expires_at] == nil assert result[:expired] == false end + + test "doesn't strips HTML tags" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "What's with the smug face?", + poll: %{ + options: [ + "", + "", + "", + "" + ], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + assert %{ + options: [ + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0} + ] + } = PollView.render("show.json", %{object: object}) + end end diff --git a/test/web/metadata/rel_me_test.exs b/test/web/metadata/rel_me_test.exs index 4107a8459..2293d6e13 100644 --- a/test/web/metadata/rel_me_test.exs +++ b/test/web/metadata/rel_me_test.exs @@ -9,13 +9,12 @@ defmodule Pleroma.Web.Metadata.Providers.RelMeTest do test "it renders all links with rel='me' from user bio" do bio = - ~s(https://some-link.com https://another-link.com - https://some-link.com https://another-link.com ) user = insert(:user, %{bio: bio}) assert RelMe.build_tags(%{user: user}) == [ - {:link, [rel: "me", href: "http://some3.com>"], []}, + {:link, [rel: "me", href: "http://some3.com"], []}, {:link, [rel: "me", href: "https://another-link.com"], []} ] end diff --git a/test/web/oauth/app_test.exs b/test/web/oauth/app_test.exs index 899af648e..993a490e0 100644 --- a/test/web/oauth/app_test.exs +++ b/test/web/oauth/app_test.exs @@ -29,5 +29,16 @@ test "gets exist app and updates scopes" do assert exist_app.id == app.id assert exist_app.scopes == ["read", "write", "follow", "push"] end + + test "has unique client_id" do + insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") + + error = + catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) + + assert %Ecto.ConstraintError{} = error + assert error.constraint == "apps_client_id_index" + assert error.type == :unique + end end end diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs index 011642c08..63b1c0eb8 100644 --- a/test/web/oauth/ldap_authorization_test.exs +++ b/test/web/oauth/ldap_authorization_test.exs @@ -7,7 +7,6 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do alias Pleroma.Repo alias Pleroma.Web.OAuth.Token import Pleroma.Factory - import ExUnit.CaptureLog import Mock @skip if !Code.ensure_loaded?(:eldap), do: :skip @@ -72,9 +71,7 @@ test "creates a new user after successful LDAP authorization" do equalityMatch: fn _type, _value -> :ok end, wholeSubtree: fn -> :ok end, search: fn _connection, _options -> - {:ok, - {:eldap_search_result, [{:eldap_entry, '', [{'mail', [to_charlist(user.email)]}]}], - []}} + {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} end, close: fn _connection -> send(self(), :close_connection) @@ -101,50 +98,6 @@ test "creates a new user after successful LDAP authorization" do end end - @tag @skip - test "falls back to the default authorization when LDAP is unavailable" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end, - simple_bind: fn _connection, _dn, ^password -> :ok end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - log = - capture_log(fn -> - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - - assert token.user_id == user.id - end) - - assert log =~ "Could not open LDAP connection: 'connect failed'" - refute_received :close_connection - end - end - @tag @skip test "disallow authorization for wrong LDAP credentials" do password = "testpassword"