Merge remote-tracking branch 'upstream/develop' into aliases

This commit is contained in:
Alex Gleason 2020-08-07 16:35:15 -05:00
commit 1a5a7ba6e8
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
186 changed files with 4752 additions and 940 deletions

2
.gitignore vendored
View file

@ -27,6 +27,8 @@ 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

View file

@ -8,9 +8,7 @@
### Environment
* Installation type:
- [ ] OTP
- [ ] From source
* Installation type (OTP or From Source):
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system:

View file

@ -6,13 +6,17 @@ 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`
- Using the `only_media` filter on timelines will now exclude reblog media
- MFR policy to set global expiration for all local Create activities
- OGP rich media parser merged with TwitterCard
- 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.
<details>
<summary>API Changes</summary>
@ -29,6 +33,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
has been simplified down to `block_from_strangers`.
- **Breaking:** Notification Settings API option for hiding push notification
contents has been renamed to `hide_notification_contents`
- Mastodon API: Added `pleroma.metadata.post_formats` to /api/v1/instance
- Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone`
- Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value.
</details>
<details>
@ -44,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.
@ -64,10 +72,14 @@ 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.
- Ability to set ActivityPub aliases for follower migration.
- "By approval" registrations mode.
- 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.
<details>
<summary>API Changes</summary>
- Mastodon API: Add pleroma.parents_visible field to statuses.
- Mastodon API: Add pleroma.parent_visible field to statuses.
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
@ -92,6 +104,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
- 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)]
@ -121,6 +135,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Follow request notifications
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
@ -188,6 +203,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking**: Using third party engines for user recommendation
<details>
<summary>API Changes</summary>
- **Breaking**: AdminAPI: migrate_from_db endpoint
</details>

View file

@ -172,7 +172,7 @@
"application/ld+json" => ["activity+json"]
}
config :tesla, adapter: Tesla.Adapter.Gun
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
@ -205,6 +205,7 @@
registrations_open: true,
invites_enabled: false,
account_activation_required: false,
account_approval_required: false,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
@ -225,8 +226,6 @@
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
@ -239,6 +238,7 @@
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
registration_reason_length: 500,
external_user_synchronization: true,
extended_nickname_format: true,
cleanup_attachments: false,
@ -252,6 +252,26 @@
number: 5,
length: 16
]
],
show_reactions: true
config :pleroma, :welcome,
direct_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
chat_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
email: [
enabled: false,
sender: nil,
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
config :pleroma, :feed,
@ -359,6 +379,7 @@
federated_timeline_removal: [],
report_removal: [],
reject: [],
followers_only: [],
accept: [],
avatar_removal: [],
banner_removal: [],
@ -377,8 +398,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,
@ -493,8 +515,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,
@ -527,16 +556,14 @@
federator_outgoing: 5
]
config :auto_linker,
opts: [
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
config :pleroma, Pleroma.Formatter,
class: false,
rel: "ugc",
new_window: false,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
@ -635,6 +662,16 @@
config :pleroma, :static_fe, enabled: false
# Example of frontend configuration
# This example will make us serve the primary frontend from the
# frontends directory within your `:pleroma, :instance, static_dir`.
# e.g., instance/static/frontends/pleroma/develop/
#
# With no frontend configuration, the bundled files from the `static` directory will
# be used.
#
# config :pleroma, :frontends, primary: %{"name" => "pleroma", "ref" => "develop"}
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
@ -696,7 +733,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: []
@ -706,6 +743,8 @@
config :pleroma, :instances_favicons, enabled: false
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"

View file

@ -661,6 +661,11 @@
type: :boolean,
description: "Require users to confirm their emails before signing in"
},
%{
key: :account_approval_required,
type: :boolean,
description: "Require users to be manually approved by an admin before signing in"
},
%{
key: :federating,
type: :boolean,
@ -778,23 +783,6 @@
type: :boolean,
description: "Enable to automatically add attachment link text to statuses"
},
%{
key: :welcome_message,
type: :string,
description:
"A message that will be sent to a newly registered users as a direct message",
suggestions: [
"Hi, @username! Welcome on board!"
]
},
%{
key: :welcome_user_nickname,
type: :string,
description: "The nickname of the local user that sends the welcome message",
suggestions: [
"lain"
]
},
%{
key: :max_report_comment_size,
type: :integer,
@ -891,6 +879,14 @@
2048
]
},
%{
key: :registration_reason_length,
type: :integer,
description: "Maximum registration reason length. Default: 500.",
suggestions: [
500
]
},
%{
key: :external_user_synchronization,
type: :boolean,
@ -959,6 +955,118 @@
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"]
},
%{
key: :show_reactions,
type: :boolean,
description: "Let favourites and emoji reactions be viewed through the API."
}
]
},
%{
group: :welcome,
type: :group,
description: "Welcome messages settings",
children: [
%{
group: :direct_message,
type: :group,
descpiption: "Direct message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sends direct message for new user after registration"
},
%{
key: :message,
type: :string,
description:
"A message that will be sent to a newly registered users as a direct message",
suggestions: [
"Hi, @username! Welcome on board!"
]
},
%{
key: :sender_nickname,
type: :string,
description: "The nickname of the local user that sends the welcome message",
suggestions: [
"lain"
]
}
]
},
%{
group: :chat_message,
type: :group,
descpiption: "Chat message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sends chat message for new user after registration"
},
%{
key: :message,
type: :string,
description:
"A message that will be sent to a 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 the welcome message",
suggestions: [
"lain"
]
}
]
},
%{
group: :email,
type: :group,
descpiption: "Email message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sends direct message for new user after registration"
},
%{
key: :sender,
type: [:string, :tuple],
description:
"The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.",
suggestions: [
{"Pleroma App", "welcome@pleroma.app"}
]
},
%{
key: :subject,
type: :string,
description:
"The subject of welcome email. Can be use EEX template with `user` and `instance_name` variables.",
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.",
suggestions: ["<h1>Hello <%= user.name%>. Welcome to <%= instance_name%></h1>"]
},
%{
key: :text,
type: :string,
description:
"The text content of welcome email. Can be use EEX template with `user` and `instance_name` variables.",
suggestions: ["Hello <%= user.name%>. \n Welcome to <%= instance_name%>\n"]
}
]
}
]
},
@ -1426,6 +1534,7 @@
group: :pleroma,
key: :mrf_simple,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
label: "MRF Simple",
type: :group,
description: "Simple ingress policies",
@ -1462,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},
@ -1492,6 +1607,7 @@
group: :pleroma,
key: :mrf_activity_expiration,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
label: "MRF Activity Expiration Policy",
type: :group,
description: "Adds automatic expiration to all local activities",
@ -1508,6 +1624,7 @@
group: :pleroma,
key: :mrf_subchain,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
label: "MRF Subchain",
type: :group,
description:
@ -1530,6 +1647,7 @@
group: :pleroma,
key: :mrf_rejectnonpublic,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
description: "RejectNonPublic drops posts with non-public visibility settings.",
label: "MRF Reject Non Public",
type: :group,
@ -1551,6 +1669,7 @@
group: :pleroma,
key: :mrf_hellthread,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
label: "MRF Hellthread",
type: :group,
description: "Block messages with excessive user mentions",
@ -1576,6 +1695,7 @@
group: :pleroma,
key: :mrf_keyword,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
label: "MRF Keyword",
type: :group,
description: "Reject or Word-Replace messages with a keyword or regex",
@ -1607,6 +1727,7 @@
group: :pleroma,
key: :mrf_mention,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
label: "MRF Mention",
type: :group,
description: "Block messages which mention a specific user",
@ -1623,6 +1744,7 @@
group: :pleroma,
key: :mrf_vocabulary,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
label: "MRF Vocabulary",
type: :group,
description: "Filter messages which belong to certain activity vocabularies",
@ -1646,6 +1768,8 @@
# %{
# group: :pleroma,
# key: :mrf_user_allowlist,
# tab: :mrf,
# related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
# type: :map,
# description:
# "The keys in this section are the domain names that the policy should apply to." <>
@ -2216,45 +2340,53 @@
]
},
%{
group: :auto_linker,
key: :opts,
group: :pleroma,
key: Pleroma.Formatter,
label: "Auto Linker",
type: :group,
description: "Configuration for the auto_linker library",
description:
"Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.",
children: [
%{
key: :class,
type: [:string, false],
type: [:string, :boolean],
description: "Specify the class to be added to the generated link. Disable to clear.",
suggestions: ["auto-linker", false]
},
%{
key: :rel,
type: [:string, false],
type: [:string, :boolean],
description: "Override the rel attribute. Disable to clear.",
suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
type: :boolean,
description: "Link URLs will open in new window/tab"
description: "Link URLs will open in a new window/tab."
},
%{
key: :truncate,
type: [:integer, false],
type: [:integer, :boolean],
description:
"Set to a number to truncate URLs longer then the number. Truncated URLs will end in `..`",
"Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`",
suggestions: [15, false]
},
%{
key: :strip_prefix,
type: :boolean,
description: "Strip the scheme prefix"
description: "Strip the scheme prefix."
},
%{
key: :extra,
type: :boolean,
description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
},
%{
key: :validate_tld,
type: [:atom, :boolean],
description:
"Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)",
suggestions: [:no_scheme, true]
}
]
},
@ -2902,8 +3034,9 @@
},
%{
group: :pleroma,
tab: :mrf,
key: :mrf_normalize_markup,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
label: "MRF Normalize Markup",
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
type: :group,
@ -2923,6 +3056,7 @@
%{
key: :restricted_nicknames,
type: {:list, :string},
description: "List of nicknames users may not register with.",
suggestions: [
".well-known",
"~",
@ -2955,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"]
}
]
},
@ -3098,8 +3238,9 @@
%{
group: :pleroma,
key: :mrf_object_age,
label: "MRF Object Age",
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
label: "MRF Object Age",
type: :group,
description:
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
@ -3400,5 +3541,30 @@
suggestions: ["s3.eu-central-1.amazonaws.com"]
}
]
},
%{
group: :pleroma,
key: :frontends,
type: :group,
description: "Installed frontends management",
children: [
%{
key: :primary,
type: :map,
description: "Primary frontend, the one that is served for all pages by default",
children: [
%{
key: "name",
type: :string,
description: "Name of the installed primary frontend"
},
%{
key: "ref",
type: :string,
description: "reference of the installed primary frontend to be used"
}
]
}
]
}
]

View file

@ -118,6 +118,10 @@
streaming_enabled: true,
public_endpoint: nil
config :tzdata, :autoupdate, :disabled
config :pleroma, :mrf, policies: []
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else

View file

@ -19,6 +19,7 @@ Configuration options:
- `local`: only local users
- `external`: only external users
- `active`: only active users
- `need_approval`: only unapproved users
- `deactivated`: only deactivated users
- `is_admin`: users with admin role
- `is_moderator`: users with moderator role
@ -46,7 +47,10 @@ Configuration options:
"local": bool,
"tags": array,
"avatar": string,
"display_name": string
"display_name": string,
"confirmation_pending": bool,
"approval_pending": bool,
"registration_reason": string,
},
...
]
@ -242,6 +246,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
}
```
## `PATCH /api/pleroma/admin/users/approve`
### Approve user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user

View file

@ -236,6 +236,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
- `pleroma.metadata.post_formats`: A list of the allowed post format types
- `vapid_public_key`: The public key needed for push messages
## Markers

View file

@ -50,7 +50,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Authentication: not required
* Params: none
* Response: Provider specific JSON, the only guaranteed parameter is `type`
* Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint"}`
* Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint", "seconds_valid": 300}`
## `/api/pleroma/delete_account`
### Delete an account

View file

@ -0,0 +1,9 @@
# Generate release environment file
```sh tab="OTP"
./bin/pleroma_ctl release_env gen
```
```sh tab="From Source"
mix pleroma.release_env gen
```

View file

@ -75,6 +75,13 @@ Feel free to contact us to be added to this list!
- Platform: Android, iOS
- Features: No Streaming
### Indigenous
- Homepage: <https://indigenous.realize.be/>
- Source Code: <https://github.com/swentel/indigenous-android/>
- Contact: [@realize.be@realize.be](@realize.be@realize.be)
- Platforms: Android
- Features: No Streaming
## Alternative Web Interfaces
### Brutaldon
- Homepage: <https://jfm.carcosa.net/projects/software/brutaldon/>

View file

@ -33,6 +33,7 @@ To add configuration to your config file, you can copy it from the base config.
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in.
* `account_approval_required`: Require users to be manually approved by an admin before signing in.
* `federating`: Enable federation with other instances.
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
@ -46,8 +47,6 @@ To add configuration to your config file, you can copy it from the base config.
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
* `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
@ -60,8 +59,44 @@ To add configuration to your config file, you can copy it from the base config.
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`).
* `account_field_name_length`: An account field name maximum length (default: `512`).
* `account_field_value_length`: An account field value maximum length (default: `2048`).
* `registration_reason_length`: Maximum registration reason length (default: `500`).
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
* `show_reactions`: Let favourites and emoji reactions be viewed through the API (default: `true`).
## Welcome
* `direct_message`: - welcome message sent as a direct message.
* `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.
* `subject`: A subject of welcome email.
* `html`: A html that will be send to a newly registered users as a email.
* `text`: A text that will be send to a newly registered users as a email.
Example:
```elixir
config :pleroma, :welcome,
direct_message: [
enabled: true,
sender_nickname: "lain",
message: "Hi, @username! Welcome on board!"
],
email: [
enabled: true,
sender: {"Pleroma App", "welcome@pleroma.app"},
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
```
## Message rewrite facility
@ -94,6 +129,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.
@ -171,6 +207,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`)
@ -817,9 +858,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`.
@ -849,6 +887,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.).
@ -934,30 +975,29 @@ Configure OAuth 2 provider capabilities:
### :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL.
### :auto_linker
### Pleroma.Formatter
Configuration for the `auto_linker` library:
Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear.
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear.
* `new_window: true` - set to false to remove `target='_blank'` attribute.
* `scheme: false` - Set to true to link urls with schema `http://google.com`.
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`.
* `strip_prefix: true` - Strip the scheme prefix.
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.).
* `class` - specify the class to be added to the generated link (default: `false`)
* `rel` - specify the rel attribute (default: `ugc`)
* `new_window` - adds `target="_blank"` attribute (default: `false`)
* `truncate` - Set to a number to truncate URLs longer then the number. Truncated URLs will end in `...` (default: `false`)
* `strip_prefix` - Strip the scheme prefix (default: `false`)
* `extra` - link URLs with rarely used schemes (magnet, ipfs, irc, etc.) (default: `true`)
* `validate_tld` - Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for urls without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't) (default: `:no_scheme`)
Example:
```elixir
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
config :pleroma, Pleroma.Formatter,
class: false,
rel: "ugc",
new_window: false,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
```
## Custom Runtime Modules (`:modules`)
@ -1019,3 +1059,25 @@ Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practi
Control favicons for instances.
* `enabled`: Allow/disallow displaying and getting instances favicons
## Frontend management
Frontends in Pleroma are swappable - you can specify which one to use here.
For now, you can set a frontend with the key `primary` and the options of `name` and `ref`. This will then make Pleroma serve the frontend from a folder constructed by concatenating the instance static path, `frontends` and the name and ref.
The key `primary` refers to the frontend that will be served by default for general requests. In the future, other frontends like the admin frontend will also be configurable here.
If you don't set anything here, the bundled frontend will be used.
Example:
```
config :pleroma, :frontends,
primary: %{
"name" => "pleroma",
"ref" => "stable"
}
```
This would serve the frontend from the the folder at `$instance_static/frontends/pleroma/stable`. You have to copy the frontend into this folder yourself. You can choose the name and ref any way you like, but they will be used by mix tasks to automate installation in the future, the name referring to the project and the ref referring to a commit.

View file

@ -5,13 +5,7 @@ The configuration of Pleroma has traditionally been managed with a config file,
## Migration to database config
1. Stop your Pleroma instance and edit your Pleroma config to enable database configuration:
```
config :pleroma, configurable_from_database: true
```
2. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened.
1. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened.
**Source:**
@ -23,76 +17,82 @@ The configuration of Pleroma has traditionally been managed with a config file,
**OTP:**
*Note: OTP users need Pleroma to be running for `pleroma_ctl` commands to work*
```
$ ./bin/pleroma_ctl config migrate_to_db
```
```
10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Migrating settings from file: /home/pleroma/config/dev.secret.exs
10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Migrating settings from file: /home/pleroma/config/dev.secret.exs
10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms
TRUNCATE config; []
10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms
TRUNCATE config; []
10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms
ALTER SEQUENCE config_id_seq RESTART; []
10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms
ALTER SEQUENCE config_id_seq RESTART; []
10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"]
10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"]
10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms
INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]]
Settings for key instance migrated.
Settings for group :pleroma migrated.
10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms
INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]]
Settings for key instance migrated.
Settings for group :pleroma migrated.
```
3. It is recommended to backup your config file now.
2. It is recommended to backup your config file now.
```
cp config/dev.secret.exs config/dev.secret.exs.orig
```
4. Now you can edit your config file and strip it down to the only settings which are not possible to control in the database. e.g., the Postgres and webserver (Endpoint) settings cannot be controlled in the database because the application needs the settings to start up and access the database.
3. Edit your Pleroma config to enable database configuration:
⚠️ **THIS IS NOT REQUIRED**
```
config :pleroma, configurable_from_database: true
```
Any settings in the database will override those in the config file, but you may find it less confusing if the setting is only declared in one place.
4. ⚠️ **THIS IS NOT REQUIRED** ⚠️
A non-exhaustive list of settings that are only possible in the config file include the following:
Now you can edit your config file and strip it down to the only settings which are not possible to control in the database. e.g., the Postgres (Repo) and webserver (Endpoint) settings cannot be controlled in the database because the application needs the settings to start up and access the database.
* config :pleroma, Pleroma.Web.Endpoint
* config :pleroma, Pleroma.Repo
* config :pleroma, configurable_from_database
* config :pleroma, :database, rum_enabled
* config :pleroma, :connections_pool
Any settings in the database will override those in the config file, but you may find it less confusing if the setting is only declared in one place.
Here is an example of a server config stripped down after migration:
A non-exhaustive list of settings that are only possible in the config file include the following:
```
use Mix.Config
* config :pleroma, Pleroma.Web.Endpoint
* config :pleroma, Pleroma.Repo
* config :pleroma, configurable\_from\_database
* config :pleroma, :database, rum_enabled
* config :pleroma, :connections_pool
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
Here is an example of a server config stripped down after migration:
```
use Mix.Config
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "pleroma",
password: "MySecretPassword",
database: "pleroma_prod",
hostname: "localhost"
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
config :pleroma, configurable_from_database: true
```
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "pleroma",
password: "MySecretPassword",
database: "pleroma_prod",
hostname: "localhost"
5. Start your instance back up and you can now access the Settings tab in AdminFE.
config :pleroma, configurable_from_database: true
```
5. Restart your instance and you can now access the Settings tab in AdminFE.
## Reverting back from database config
1. Stop your Pleroma instance.
2. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened.
1. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened.
**Source:**
@ -110,14 +110,16 @@ config :pleroma, configurable_from_database: true
```
10:26:30.593 [debug] QUERY OK source="config" db=9.8ms decode=1.2ms queue=26.0ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
10:26:30.659 [debug] QUERY OK source="config" db=1.1ms idle=80.7ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Database configuration settings have been saved to config/dev.exported_from_db.secret.exs
```
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Database configuration settings have been saved to config/dev.exported_from_db.secret.exs
```
3. The in-database configuration still exists, but it will not be used if you remove `config :pleroma, configurable_from_database: true` from your config.
2. Remove `config :pleroma, configurable_from_database: true` from your config. The in-database configuration still exists, but it will not be used. Future migrations will erase the database config before importing your config file again.
3. Restart your instance.
## Debugging

View file

@ -121,6 +121,9 @@ 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"
@ -131,7 +134,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 "./bin/pleroma daemon"
su pleroma -s $SHELL -lc "export $(cat /opt/pleroma/config/pleroma.env); ./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
@ -200,6 +203,7 @@ 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
@ -275,4 +279,3 @@ This will create an account withe the username of 'joeuser' with the email addre
## Questions
Questions about the installation or didnt 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**.

View file

@ -8,6 +8,7 @@ pidfile="/var/run/pleroma.pid"
directory=/opt/pleroma
healthcheck_delay=60
healthcheck_timer=30
export $(cat /opt/pleroma/config/pleroma.env)
: ${pleroma_port:-4000}

View file

@ -17,6 +17,8 @@ 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

View file

@ -24,8 +24,10 @@ def start_pleroma do
Application.put_env(:logger, :console, level: :debug)
end
adapter = Application.get_env(:tesla, :adapter)
apps =
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
if adapter == Tesla.Adapter.Gun do
[:gun | @apps]
else
[:hackney | @apps]
@ -33,11 +35,14 @@ def start_pleroma do
Enum.each(apps, &Application.ensure_all_started/1)
children = [
Pleroma.Repo,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint
]
children =
[
Pleroma.Repo,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint,
{Oban, Pleroma.Config.get(Oban)}
] ++
http_children(adapter)
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
@ -115,4 +120,11 @@ def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
defp http_children(Tesla.Adapter.Gun) do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
defp http_children(_), do: []
end

View file

@ -0,0 +1,76 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

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

View file

@ -16,7 +16,9 @@ defmodule VerifyError, do: defexception([:message])
@spec verify!() :: :ok | VerifyError.t()
def verify! do
:ok
|> check_confirmation_accounts!
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
|> handle_result()
end
@ -24,6 +26,40 @@ def verify! do
defp handle_result(:ok), do: :ok
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do
Logger.error("""
To send welcome email do you need to enable mail.
\nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true
""")
{:error, "The mail disabled."}
else
:ok
end
end
defp check_welcome_message_config!(result), do: result
# Checks account confirmation email
#
def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
Logger.error(
"Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
)
{:error,
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
else
:ok
end
end
def check_confirmation_accounts!(result), do: result
# Checks for pending migrations.
#
def check_migrations_applied!(:ok) do

View file

@ -21,7 +21,8 @@ def new do
type: :kocaptcha,
token: json_resp["token"],
url: endpoint <> json_resp["url"],
answer_data: json_resp["md5"]
answer_data: json_resp["md5"],
seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
}
end
end

View file

@ -17,7 +17,8 @@ def new do
type: :native,
token: token(),
url: "data:image/png;base64," <> Base.encode64(img_binary),
answer_data: answer_data
answer_data: answer_data,
seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
}
end
end

View file

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

View file

@ -156,7 +156,6 @@ defp only_full_update?(%ConfigDB{group: group, key: key}) do
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]

View file

@ -55,6 +55,24 @@ def warn do
mrf_user_allowlist()
check_old_mrf_config()
check_media_proxy_whitelist_config()
check_welcome_message_config()
end
def check_welcome_message_config do
instance_config = Pleroma.Config.get([:instance])
use_old_config =
Keyword.has_key?(instance_config, :welcome_user_nickname) or
Keyword.has_key?(instance_config, :welcome_message)
if use_old_config do
Logger.error("""
!!!DEPRECATION WARNING!!!
Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace:
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
""")
end
end
def check_old_mrf_config do

View file

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Helpers do
alias Pleroma.Config
def instance_name, do: Config.get([:instance, :name])
defp instance_notify_email do
Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
end
def sender do
{instance_name(), instance_notify_email()}
end
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Emails.AdminEmail do
import Swoosh.Email
alias Pleroma.Config
alias Pleroma.HTML
alias Pleroma.Web.Router.Helpers
defp instance_config, do: Config.get(:instance)
@ -82,4 +83,18 @@ def report(to, reporter, account, statuses, comment) do
|> subject("#{instance_name()} Report")
|> html_body(html_body)
end
def new_unapproved_registration(to, account) do
html_body = """
<p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
<blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
"""
new()
|> to({to.name, to.email})
|> from({instance_name(), instance_notify_email()})
|> subject("New account up for review on #{instance_name()} (@#{account.nickname})")
|> html_body(html_body)
end
end

View file

@ -12,17 +12,22 @@ defmodule Pleroma.Emails.UserEmail do
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
defp instance_name, do: Config.get([:instance, :name])
defp sender do
email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
{instance_name(), email}
end
import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0]
defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email}
defp recipient(%User{} = user), do: recipient(user.email, user.name)
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
def welcome(user, opts \\ %{}) do
new()
|> to(recipient(user))
|> from(Map.get(opts, :sender, sender()))
|> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
|> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
|> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
end
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)

View file

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

View file

@ -10,11 +10,15 @@ defmodule Pleroma.Formatter do
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@auto_linker_config hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4,
scheme: true
defp linkify_opts do
Pleroma.Config.get(Pleroma.Formatter) ++
[
hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
]
end
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
@ -80,19 +84,19 @@ def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
options = linkify_opts() ++ options
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
{text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
@ -111,9 +115,9 @@ def mentions_escape(text, options \\ []) do
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
Linkify.link(mentions, options) <> Linkify.link(rest, options)
else
AutoLinker.link(text, options)
Linkify.link(text, options)
end
end

View file

@ -96,16 +96,18 @@ def response("") do
def response("/main/public") do
posts =
ActivityPub.fetch_public_activities(%{"type" => ["Create"], "local_only" => true})
|> render_activities
%{type: ["Create"], local_only: true}
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
end
def response("/main/all") do
posts =
ActivityPub.fetch_public_activities(%{"type" => ["Create"]})
|> render_activities
%{type: ["Create"]}
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
end
@ -130,13 +132,14 @@ def response("/notices/" <> id) do
def response("/users/" <> nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
params = %{
"type" => ["Create"],
"actor_id" => user.ap_id
type: ["Create"],
actor_id: user.ap_id
}
activities =
ActivityPub.fetch_public_activities(params)
|> render_activities
params
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
else

View file

@ -10,6 +10,7 @@ def children do
]
end
@spec get_conn(URI.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def get_conn(uri, opts) do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
@ -19,7 +20,7 @@ def get_conn(uri, opts) do
get_gun_pid_from_worker(worker_pid, true)
[{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
GenServer.cast(worker_pid, {:add_client, self(), false})
GenServer.call(worker_pid, :add_client)
{:ok, gun_pid}
[] ->
@ -45,7 +46,7 @@ defp get_gun_pid_from_worker(worker_pid, register) do
# so instead we use cast + monitor
ref = Process.monitor(worker_pid)
if register, do: GenServer.cast(worker_pid, {:add_client, self(), true})
if register, do: GenServer.cast(worker_pid, {:add_client, self()})
receive do
{:conn_pid, pid} ->
@ -54,12 +55,14 @@ defp get_gun_pid_from_worker(worker_pid, register) do
{:DOWN, ^ref, :process, ^worker_pid, reason} ->
case reason do
{:shutdown, error} -> error
{:shutdown, {:error, _} = error} -> error
{:shutdown, error} -> {:error, error}
_ -> {:error, reason}
end
end
end
@spec release_conn(pid()) :: :ok
def release_conn(conn_pid) do
# :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid ->
# worker_pid end)
@ -70,7 +73,7 @@ def release_conn(conn_pid) do
case query_result do
[worker_pid] ->
GenServer.cast(worker_pid, {:remove_client, self()})
GenServer.call(worker_pid, :remove_client)
[] ->
:ok

View file

@ -36,7 +36,24 @@ def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
end
@impl true
def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do
def handle_cast({:add_client, client_pid}, state) do
case handle_call(:add_client, {client_pid, nil}, state) do
{:reply, conn_pid, state, :hibernate} ->
send(client_pid, {:conn_pid, conn_pid})
{:noreply, state, :hibernate}
end
end
@impl true
def handle_cast({:remove_client, client_pid}, state) do
case handle_call(:remove_client, {client_pid, nil}, state) do
{:reply, _, state, :hibernate} ->
{:noreply, state, :hibernate}
end
end
@impl true
def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do
time = :erlang.monotonic_time(:millisecond)
{{conn_pid, _, _, _}, _} =
@ -44,8 +61,6 @@ def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) d
{conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
end)
if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid})
state =
if state.timer != nil do
Process.cancel_timer(state[:timer])
@ -57,11 +72,11 @@ def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) d
ref = Process.monitor(client_pid)
state = put_in(state.client_monitors[client_pid], ref)
{:noreply, state, :hibernate}
{:reply, conn_pid, state, :hibernate}
end
@impl true
def handle_cast({:remove_client, client_pid}, %{key: key} = state) do
def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do
{{_conn_pid, used_by, _crf, _last_reference}, _} =
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
{conn_pid, List.delete(used_by, client_pid), crf, last_reference}
@ -78,7 +93,7 @@ def handle_cast({:remove_client, client_pid}, %{key: key} = state) do
nil
end
{:noreply, %{state | timer: timer}, :hibernate}
{:reply, :ok, %{state | timer: timer}, :hibernate}
end
@impl true
@ -102,22 +117,13 @@ def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_me
@impl true
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
# Sometimes the client is dead before we demonitor it in :remove_client, so the message
# arrives anyway
:telemetry.execute(
[:pleroma, :connection_pool, :client_death],
%{client_pid: pid, reason: reason},
%{key: state.key}
)
case state.client_monitors[pid] do
nil ->
{:noreply, state, :hibernate}
_ref ->
:telemetry.execute(
[:pleroma, :connection_pool, :client_death],
%{client_pid: pid, reason: reason},
%{key: state.key}
)
handle_cast({:remove_client, pid}, state)
end
handle_cast({:remove_client, pid}, state)
end
# LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478

View file

@ -69,7 +69,8 @@ def request(method, url, body, headers, options) when is_binary(url) do
request = build_request(method, headers, options, url, body, params)
adapter = Application.get_env(:tesla, :adapter)
client = Tesla.client([Pleroma.HTTP.Middleware.FollowRedirects], adapter)
client = Tesla.client(adapter_middlewares(adapter), adapter)
maybe_limit(
fn ->
@ -107,4 +108,10 @@ defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do
defp maybe_limit(fun, _, _) do
fun.()
end
defp adapter_middlewares(Tesla.Adapter.Gun) do
[Pleroma.HTTP.Middleware.FollowRedirects]
end
defp adapter_middlewares(_), do: []
end

View file

@ -34,10 +34,12 @@ def url(request, u), do: %{request | url: u}
@spec headers(Request.t(), Request.headers()) :: Request.t()
def headers(request, headers) do
headers_list =
if Pleroma.Config.get([:http, :send_user_agent]) do
with true <- Pleroma.Config.get([:http, :send_user_agent]),
nil <- Enum.find(headers, fn {key, _val} -> String.downcase(key) == "user-agent" end) do
[{"user-agent", Pleroma.Application.user_agent()} | headers]
else
headers
_ ->
headers
end
%{request | headers: headers_list}

View file

@ -409,6 +409,17 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "approve",
"subject" => users
}
}) do
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{

View file

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

View file

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

View file

@ -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}
@ -124,6 +143,10 @@ def fetch_object_from_id!(id, options \\ []) do
{:error, "Object has been deleted"} ->
nil
{:reject, reason} ->
Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
nil
e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil

View file

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.FrontendStatic do
require Pleroma.Constants
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
"""
@behaviour Plug
def file_path(path, frontend_type \\ :primary) do
if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
Path.join([
instance_static_path,
"frontends",
configuration["name"],
configuration["ref"],
path
])
else
nil
end
end
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
|> Plug.Static.init()
end
def call(conn, opts) do
frontend_type = Map.get(opts, :frontend_type, :primary)
path = file_path("", frontend_type)
if path do
conn
|> call_static(opts, path)
else
conn
end
end
defp call_static(conn, opts, from) do
opts =
opts
|> Map.put(:from, from)
Plug.Static.call(conn, opts)
end
end

View file

@ -16,28 +16,24 @@ def file_path(path) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
if File.exists?(instance_path) do
instance_path
else
frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary)
(File.exists?(instance_path) && instance_path) ||
(frontend_path && File.exists?(frontend_path) && frontend_path) ||
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
end
end
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|> Keyword.put(:at, "/__unconfigured_instance_static_plug")
|> Plug.Static.init()
end
for only <- Pleroma.Constants.static_only_files() do
at = Plug.Router.Utils.split("/")
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
call_static(
conn,
opts,
unquote(at),
Pleroma.Config.get([:instance, :static_dir], "instance/static")
)
end
@ -47,11 +43,10 @@ def call(conn, _) do
conn
end
defp call_static(conn, opts, at, from) do
defp call_static(conn, opts, from) do
opts =
opts
|> Map.put(:from, from)
|> Map.put(:at, at)
Plug.Static.call(conn, opts)
end

View file

@ -5,6 +5,8 @@
defmodule Pleroma.ReverseProxy.Client.Tesla do
@behaviour Pleroma.ReverseProxy.Client
alias Pleroma.Gun.ConnectionPool
@type headers() :: [{String.t(), String.t()}]
@type status() :: pos_integer()
@ -31,6 +33,8 @@ def request(method, url, headers, body, opts \\ []) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}
else
conn_pid = response.opts[:adapter][:conn]
ConnectionPool.release_conn(conn_pid)
{:ok, response.status, response.headers}
end
else
@ -41,15 +45,8 @@ def request(method, url, headers, body, opts \\ []) do
@impl true
@spec stream_body(map()) ::
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
def stream_body(%{pid: pid, opts: opts, fin: true}) do
# if connection was reused, but in tesla were redirects,
# tesla returns new opened connection, which must be closed manually
if opts[:old_conn], do: Tesla.Adapter.Gun.close(pid)
# if there were redirects we need to checkout old conn
conn = opts[:old_conn] || opts[:conn]
if conn, do: :ok = Pleroma.Gun.ConnectionPool.release_conn(conn)
def stream_body(%{pid: pid, fin: true}) do
ConnectionPool.release_conn(pid)
:done
end
@ -74,8 +71,7 @@ defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
@impl true
@spec close(map) :: :ok | no_return()
def close(%{pid: pid}) do
adapter = check_adapter()
adapter.close(pid)
ConnectionPool.release_conn(pid)
end
defp check_adapter do

View file

@ -165,6 +165,9 @@ defp request(method, url, headers, opts) do
{:ok, code, _, _} ->
{:error, {:invalid_http_response, code}}
{:ok, code, _} ->
{:error, {:invalid_http_response, code}}
{:error, error} ->
{:error, error}
end

View file

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

View file

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

View file

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

View file

@ -42,7 +42,12 @@ defmodule Pleroma.User do
require Logger
@type t :: %__MODULE__{}
@type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
@type account_status ::
:active
| :deactivated
| :password_reset_pending
| :confirmation_pending
| :approval_pending
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@ -109,6 +114,8 @@ defmodule Pleroma.User do
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:approval_pending, :boolean, default: false)
field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
@ -265,6 +272,7 @@ def binary_id(%User{} = user), do: binary_id(user.id)
@spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{approval_pending: true}), do: :approval_pending
def account_status(%User{confirmation_pending: true}) do
if Config.get([:instance, :account_activation_required]) do
@ -633,9 +641,38 @@ 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)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? =
@ -645,8 +682,16 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
opts[:need_confirmation]
end
need_approval? =
if is_nil(opts[:need_approval]) do
Config.get([:instance, :account_approval_required])
else
opts[:need_approval]
end
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
|> approval_changeset(need_approval: need_approval?)
|> cast(params, [
:bio,
:raw_bio,
@ -656,17 +701,28 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
:password,
:password_confirmation,
:emoji,
:accepts_chat_messages
:accepts_chat_messages,
:registration_reason
])
|> 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)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
@ -716,27 +772,62 @@ def register(%Ecto.Changeset{} = changeset) do
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(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
end
def try_send_confirmation_email(%User{} = user) do
if user.confirmation_pending &&
Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
def send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def try_send_confirmation_email(users) do
Enum.each(users, &try_send_confirmation_email/1)
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)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_email(_), do: {:ok, :noop}
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def try_send_confirmation_email(_), do: {:ok, :noop}
@spec send_confirmation_email(Uset.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
user
end
def needs_update?(%User{local: true}), do: false
@ -1472,6 +1563,19 @@ def deactivate(%User{} = user, status) do
end
end
def approve(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- approve(user), do: user
end)
end)
end
def approve(%User{} = user) do
change(user, approval_pending: false)
|> update_and_set_cache()
end
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
@ -1498,12 +1602,17 @@ defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate
defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user)
if status == :confirmation_pending do
delete_and_invalidate_cache(user)
else
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
case status do
:confirmation_pending ->
delete_and_invalidate_cache(user)
:approval_pending ->
delete_and_invalidate_cache(user)
_ ->
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
end
end
@ -2156,6 +2265,12 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
cast(user, params, [:confirmation_pending, :confirmation_token])
end
@spec approval_changeset(User.t(), keyword()) :: Changeset.t()
def approval_changeset(user, need_approval: need_approval?) do
params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
cast(user, params, [:approval_pending])
end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)

View file

@ -42,6 +42,7 @@ defmodule Pleroma.User.Query do
external: boolean(),
active: boolean(),
deactivated: boolean(),
need_approval: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
@ -146,6 +147,10 @@ defp compose_query({:deactivated, true}, query) do
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:need_approval, _}, query) do
where(query, [u], u.approval_pending)
end
defp compose_query({:followers, %User{id: id}}, query) do
query
|> where([u], u.id != ^id)

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeEmail do
@moduledoc """
The module represents the functions to send welcome email.
"""
alias Pleroma.Config
alias Pleroma.Emails
alias Pleroma.User
import Pleroma.Config.Helpers, only: [instance_name: 0]
@spec enabled?() :: boolean()
def enabled?, do: Config.get([:welcome, :email, :enabled], false)
@spec send_email(User.t()) :: {:ok, Oban.Job.t()}
def send_email(%User{} = user) do
user
|> Emails.UserEmail.welcome(email_options(user))
|> Emails.Mailer.deliver_async()
end
defp email_options(user) do
bindings = [user: user, instance_name: instance_name()]
%{}
|> add_sender(Config.get([:welcome, :email, :sender], nil))
|> add_option(:subject, bindings)
|> add_option(:html, bindings)
|> add_option(:text, bindings)
end
defp add_option(opts, option, bindings) do
[:welcome, :email, option]
|> Config.get(nil)
|> eval_string(bindings)
|> merge_options(opts, option)
end
defp add_sender(opts, {_name, _email} = sender) do
merge_options(sender, opts, :sender)
end
defp add_sender(opts, sender) when is_binary(sender) do
add_sender(opts, {instance_name(), sender})
end
defp add_sender(opts, _), do: opts
defp merge_options(nil, options, _option), do: options
defp merge_options(value, options, option) do
Map.merge(options, %{option => value})
end
defp eval_string(nil, _), do: nil
defp eval_string("", _), do: nil
defp eval_string(str, bindings), do: EEx.eval_string(str, bindings)
end

View file

@ -3,32 +3,45 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeMessage do
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
def post_welcome_message_to_user(user) do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
visibility: "direct",
status: "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}
end
@spec enabled?() :: boolean()
def enabled?, do: Config.get([:welcome, :direct_message, :enabled], false)
@spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
def post_message(user) do
[:welcome, :direct_message, :sender_nickname]
|> Config.get(nil)
|> fetch_sender()
|> do_post(user, welcome_message())
end
defp welcome_user do
with nickname when is_binary(nickname) <-
Pleroma.Config.get([:instance, :welcome_user_nickname]),
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
defp do_post(%User{} = sender, %User{nickname: nickname}, message)
when is_binary(message) do
CommonAPI.post(
sender,
%{
visibility: "direct",
status: "@#{nickname}\n#{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
Pleroma.Config.get([:instance, :welcome_message])
Config.get([:welcome, :direct_message, :message], nil)
end
end

View file

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

View file

@ -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),
@ -1370,6 +1359,10 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
{:error, {:reject, reason} = e} ->
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
{:error, e}
{:error, e} ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}

View file

@ -80,6 +80,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 +95,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 +123,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,

View file

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

View file

@ -27,7 +27,8 @@ def filter_by_summary(
def filter_by_summary(_in_reply_to, child), do: child
def filter(%{"type" => "Create", "object" => child_object} = object) do
def filter(%{"type" => "Create", "object" => child_object} = object)
when is_map(child_object) do
child =
child_object["inReplyTo"]
|> Object.normalize(child_object["inReplyTo"])

View file

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

View file

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

View file

@ -9,17 +9,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
the system.
"""
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
alias Pleroma.User
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
@ -71,6 +75,12 @@ def validate(%{"type" => "Undo"} = object, meta) do
|> UndoValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
undone_object = Activity.get_by_ap_id(object["object"])
meta =
meta
|> Keyword.put(:object_data, undone_object.data)
{:ok, object, meta}
end
end
@ -105,17 +115,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} <-
@ -127,12 +160,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
@ -141,8 +190,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()

View file

@ -0,0 +1,65 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

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

View file

@ -0,0 +1,133 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

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

View file

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

View file

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,127 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

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

View file

@ -52,6 +52,13 @@ defp maybe_federate(%Activity{} = activity, meta) do
do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
if !do_not_federate && local do
activity =
if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)}
else
activity
end
Federator.publish(activity)
{:ok, :federated}
else

View file

@ -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
@ -19,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
alias Pleroma.Workers.BackgroundWorker
def handle(object, meta \\ [])
@ -135,10 +137,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
@ -268,9 +286,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)

View file

@ -157,7 +157,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 \\ [])
@ -178,7 +183,7 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
|> Map.drop(["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
else
@ -240,13 +245,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
@ -419,6 +428,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 +489,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
@ -613,6 +633,17 @@ def handle_incoming(
|> handle_incoming(options)
end
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype}} = data,
_options
)
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}
end
end
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
_options

View file

@ -719,15 +719,18 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do
case Activity.get_by_ap_id_with_object(id) do
%Activity{} = activity ->
activity_actor = User.get_by_ap_id(activity.object.data["actor"])
%{
"type" => "Note",
"id" => activity.data["id"],
"content" => activity.object.data["content"],
"published" => activity.object.data["published"],
"actor" =>
AccountView.render("show.json", %{
user: User.get_by_ap_id(activity.object.data["actor"])
})
AccountView.render(
"show.json",
%{user: activity_actor, skip_visibility_check: true}
)
}
_ ->

View file

@ -44,6 +44,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:user_toggle_activation,
:user_activate,
:user_deactivate,
:user_approve,
:tag_users,
:untag_users,
:right_add,
@ -303,6 +304,21 @@ def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nickname
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "approve"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: updated_users})
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
@ -345,12 +361,16 @@ def list_users(conn, params) do
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
json(
conn,
AccountView.render("index.json", users: users, count: count, page_size: page_size)
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
end
@filters ~w(local external active deactivated is_admin is_moderator)
@filters ~w(local external active deactivated need_approval is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
@ -616,29 +636,24 @@ def reload_emoji(conn, _params) do
end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
User.toggle_confirmation(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "confirm_email"
})
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
json(conn, "")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
users =
Enum.map(nicknames, fn nickname ->
nickname
|> User.get_cached_by_nickname()
|> User.send_confirmation_email()
end)
User.try_send_confirmation_email(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "resend_confirmation_email"
})
ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
json(conn, "")
end

View file

@ -77,7 +77,9 @@ def render("show.json", %{user: user}) do
"roles" => User.roles(user),
"tags" => user.tags || [],
"confirmation_pending" => user.confirmation_pending,
"url" => user.uri || user.ap_id
"approval_pending" => user.approval_pending,
"url" => user.uri || user.ap_id,
"registration_reason" => user.registration_reason
}
end
@ -105,7 +107,7 @@ def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, e
end
def merge_account_views(%User{} = user) do
MastodonAPI.AccountView.render("show.json", %{user: user})
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
end

View file

@ -159,6 +159,7 @@ def followers_operation do
"Accounts which follow the given account, if network is not hidden by the account owner.",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
with_relationships_param() | pagination_params()
],
responses: %{
@ -177,6 +178,7 @@ def following_operation do
"Accounts which the given account is following, if network is not hidden by the account owner.",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
with_relationships_param() | pagination_params()
],
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
@ -447,21 +449,32 @@ defp create_request do
}
end
# TODO: This is actually a token respone, but there's no oauth operation file yet.
# Note: this is a token response (if login succeeds!), but there's no oauth operation file yet.
defp create_response do
%Schema{
title: "AccountCreateResponse",
description: "Response schema for an account",
type: :object,
properties: %{
# The response when auto-login on create succeeds (token is issued):
token_type: %Schema{type: :string},
access_token: %Schema{type: :string},
refresh_token: %Schema{type: :string},
scope: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"},
me: %Schema{type: :string},
expires_in: %Schema{type: :integer}
expires_in: %Schema{type: :integer},
#
# The response when registration succeeds but auto-login fails (no token):
identifier: %Schema{type: :string},
message: %Schema{type: :string}
},
required: [],
# Note: example of successful registration with failed login response:
# example: %{
# "identifier" => "missing_confirmed_email",
# "message" => "You have been registered. Please check your email for further instructions."
# },
example: %{
"token_type" => "Bearer",
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",

View file

@ -300,11 +300,11 @@ def chat_messages_response do
"content" => "Check this out :firefox:",
"id" => "13",
"chat_id" => "1",
"actor_id" => "someflakeid",
"account_id" => "someflakeid",
"unread" => false
},
%{
"actor_id" => "someflakeid",
"account_id" => "someflakeid",
"content" => "Whats' up?",
"id" => "12",
"chat_id" => "1",

View file

@ -31,6 +31,7 @@ def index_operation do
}
end
# Supporting domain query parameter is deprecated in Mastodon API
def create_operation do
%Operation{
tags: ["domain_blocks"],
@ -45,11 +46,13 @@ def create_operation do
""",
operationId: "DomainBlockController.create",
requestBody: domain_block_request(),
parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{200 => empty_object_response()}
}
end
# Supporting domain query parameter is deprecated in Mastodon API
def delete_operation do
%Operation{
tags: ["domain_blocks"],
@ -57,6 +60,7 @@ def delete_operation do
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
requestBody: domain_block_request(),
parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@ -71,10 +75,9 @@ defp domain_block_request do
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain]
}
},
required: true,
required: false,
example: %{
"domain" => "facebook.com"
}

View file

@ -19,13 +19,46 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
content: %Schema{type: :string, nullable: true},
created_at: %Schema{type: :string, format: :"date-time"},
emojis: %Schema{type: :array},
attachment: %Schema{type: :object, nullable: true}
attachment: %Schema{type: :object, nullable: true},
card: %Schema{
type: :object,
nullable: true,
description: "Preview card for links included within status content",
required: [:url, :title, :description, :type],
properties: %{
type: %Schema{
type: :string,
enum: ["link", "photo", "video", "rich"],
description: "The type of the preview card"
},
provider_name: %Schema{
type: :string,
nullable: true,
description: "The provider of the original resource"
},
provider_url: %Schema{
type: :string,
format: :uri,
description: "A link to the provider of the original resource"
},
url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
image: %Schema{
type: :string,
nullable: true,
format: :uri,
description: "Preview thumbnail"
},
title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"}
}
}
},
example: %{
"account_id" => "someflakeid",
"chat_id" => "1",
"content" => "hey you again",
"created_at" => "2020-04-21T15:06:45.000Z",
"card" => nil,
"emojis" => [
%{
"static_url" => "https://dontbulling.me/emoji/Firefox.gif",

View file

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

View file

@ -4,8 +4,10 @@
defmodule Pleroma.Web.ChatChannel do
use Phoenix.Channel
alias Pleroma.User
alias Pleroma.Web.ChatChannel.ChatChannelState
alias Pleroma.Web.MastodonAPI.AccountView
def join("chat:public", _message, socket) do
send(self(), :after_join)
@ -22,9 +24,9 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}}
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
author = User.get_cached_by_nickname(user_name)
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
author_json = AccountView.render("show.json", user: author, skip_visibility_check: true)
message = ChatChannelState.add_message(%{text: text, author: author})
message = ChatChannelState.add_message(%{text: text, author: author_json})
broadcast!(socket, "new_msg", message)
end

View file

@ -308,18 +308,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 +341,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)

View file

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

View file

@ -28,6 +28,17 @@ defmodule Pleroma.Web.Endpoint do
}
)
# Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest

View file

@ -47,7 +47,7 @@ def feed(conn, %{"nickname" => nickname} = params) do
"atom"
end
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities =
%{
type: ["Create"],
@ -71,6 +71,7 @@ def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end
def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do

View file

@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@ -100,11 +100,34 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
{:ok, user} <- TwitterAPI.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
{:error, error} -> json_response(conn, :bad_request, %{error: error})
{:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{
message: "You have been registered. Please check your email for further instructions.",
identifier: "missing_confirmed_email"
})
{:login, {:account_status, :approval_pending}} ->
json_response(conn, :ok, %{
message:
"You have been registered. You'll be able to log in once your account is approved.",
identifier: "awaiting_approval"
})
{:login, _} ->
json_response(conn, :ok, %{
message:
"You have been registered. Some post-registration steps may be pending. " <>
"Please log in manually.",
identifier: "manual_login_required"
})
{:error, error} ->
json_response(conn, :bad_request, %{error: error})
end
end

View file

@ -32,9 +32,19 @@ def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn,
json(conn, %{})
end
def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
end

View file

@ -93,7 +93,6 @@ defp resource_search(_, "accounts", query, options) do
AccountView.render("index.json",
users: accounts,
for: options[:for_user],
as: :user,
embed_relationships: options[:embed_relationships]
)
end

View file

@ -314,7 +314,8 @@ def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
@doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
users =

View file

@ -27,21 +27,40 @@ def render("index.json", %{users: users} = opts) do
UserRelationship.view_relationships_option(reading_user, users)
end
opts = Map.put(opts, :relationships, relationships_opt)
opts =
opts
|> Map.merge(%{relationships: relationships_opt, as: :user})
|> Map.delete(:users)
users
|> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end
def render("show.json", %{user: user} = opts) do
if User.visible_for(user, opts[:for]) == :visible do
@doc """
Renders specified user account.
:skip_visibility_check option skips visibility check and renders any user (local or remote)
regardless of [:pleroma, :restrict_unauthenticated] setting.
:for option specifies the requester and can be a User record or nil.
Only use `user: user, for: user` when `user` is the actual requester of own profile.
"""
def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
do_render("show.json", opts)
end
def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
if User.visible_for(user, for_user_or_nil) == :visible do
do_render("show.json", opts)
else
%{}
end
end
def render("show.json", _) do
raise "In order to prevent account accessibility issues, " <>
":skip_visibility_check or :for option is required."
end
def render("mention.json", %{user: user}) do
%{
id: to_string(user.id),

View file

@ -38,7 +38,7 @@ def render("participation.json", %{participation: participation, for: user}) do
%{
id: participation.id |> to_string(),
accounts: render(AccountView, "index.json", users: users, as: :user),
accounts: render(AccountView, "index.json", users: users, for: user),
unread: !participation.read,
last_status:
render(StatusView, "show.json",

View file

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

View file

@ -26,6 +26,7 @@ def render("show.json", _) do
thumbnail: Keyword.get(instance, :instance_thumbnail),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits),
@ -41,7 +42,8 @@ def render("show.json", _) do
account_activation_required: Keyword.get(instance, :account_activation_required),
features: features(),
federation: federation(),
fields_limits: fields_limits()
fields_limits: fields_limits(),
post_formats: Config.get([:instance, :allowed_post_formats])
},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}

View file

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

View file

@ -297,13 +297,17 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do
Enum.map(emoji_reactions, fn [emoji, users] ->
%{
name: emoji,
count: length(users),
me: !!(opts[:for] && opts[:for].ap_id in users)
}
Enum.map(emoji_reactions, fn
[emoji, users] when is_list(users) ->
build_emoji_map(emoji, users, opts[:for])
{emoji, users} when is_list(users) ->
build_emoji_map(emoji, users, opts[:for])
_ ->
nil
end)
|> Enum.reject(&is_nil/1)
else
_ -> []
end
@ -545,4 +549,12 @@ defp present?(_), do: true
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
defp build_emoji_map(emoji, users, current_user) do
%{
name: emoji,
count: length(users),
me: !!(current_user && current_user.ap_id in users)
}
end
end

View file

@ -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"],
@ -260,11 +267,8 @@ def token_exchange(
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
{:ok, app} <- Token.Utils.fetch_app(conn),
{:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
error ->
@ -337,6 +341,16 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirm
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
render_error(
conn,
:forbidden,
"Your account is awaiting approval.",
%{},
"awaiting_approval"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn)
end
@ -512,6 +526,8 @@ def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} =
end
end
defp do_create_authorization(conn, auth_attrs, user \\ nil)
defp do_create_authorization(
%Plug.Conn{} = conn,
%{
@ -521,19 +537,37 @@ defp do_create_authorization(
"redirect_uri" => redirect_uri
} = auth_attrs
},
user \\ nil
user
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs),
{:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
{:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
{:ok, auth, user}
end
end
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
when is_list(requested_scopes) do
with {:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, requested_scopes),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
{:ok, auth}
end
end
# Note: intended to be a private function but opened for AccountController that logs in on signup
@doc "If checks pass, creates authorization and token for given user, app and requested scopes."
def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
{:ok, token}
end
end
# Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
@ -550,12 +584,15 @@ defp build_and_response_mfa_token(user, auth) do
end
end
@spec validate_scopes(App.t(), map()) ::
@spec validate_scopes(App.t(), map() | list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(%App{} = app, params) do
params
|> Scopes.fetch_scopes(app.scopes)
|> Scopes.validate(app.scopes)
defp validate_scopes(%App{} = app, params) when is_map(params) do
requested_scopes = Scopes.fetch_scopes(params, app.scopes)
validate_scopes(app, requested_scopes)
end
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
Scopes.validate(requested_scopes, app.scopes)
end
def default_redirect_uri(%App{} = app) do

View file

@ -89,11 +89,11 @@ def post_chat_message(
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
conn
|> put_view(MessageReferenceView)
|> render("show.json", for: user, chat_message_reference: cm_ref)
|> render("show.json", chat_message_reference: cm_ref)
end
end
def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{
id: chat_id,
message_id: message_id
}) do
@ -104,12 +104,15 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
conn
|> put_view(MessageReferenceView)
|> render("show.json", for: user, chat_message_reference: cm_ref)
|> render("show.json", chat_message_reference: cm_ref)
end
end
def mark_as_read(
%{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
%{
body_params: %{last_read_id: last_read_id},
assigns: %{user: %{id: user_id}}
} = conn,
%{id: id}
) do
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
@ -121,7 +124,7 @@ def mark_as_read(
end
end
def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
cm_refs =
chat
@ -130,7 +133,7 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = para
conn
|> put_view(MessageReferenceView)
|> render("index.json", for: user, chat_message_references: cm_refs)
|> render("index.json", chat_message_references: cm_refs)
else
_ ->
conn

View file

@ -21,8 +21,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
]
)
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation

View file

@ -25,7 +25,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
Object.normalize(activity) do
reactions = filter(reactions, params)

View file

@ -14,7 +14,7 @@ def render(
%{
chat_message_reference: %{
id: id,
object: %{data: chat_message},
object: %{data: chat_message} = object,
chat_id: chat_id,
unread: unread
}
@ -30,7 +30,12 @@ def render(
attachment:
chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
unread: unread
unread: unread,
card:
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
)
}
end

View file

@ -15,10 +15,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
def render("show.json", %{chat: %Chat{} = chat} = opts) do
recipient = User.get_cached_by_ap_id(chat.recipient)
last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
account_view_opts = account_view_opts(opts, recipient)
%{
id: chat.id |> to_string(),
account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
account: AccountView.render("show.json", account_view_opts),
unread: MessageReference.unread_count_for_chat(chat),
last_message:
last_message &&
@ -27,7 +28,17 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do
}
end
def render("index.json", %{chats: chats}) do
render_many(chats, __MODULE__, "show.json")
def render("index.json", %{chats: chats} = opts) do
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
end
defp account_view_opts(opts, recipient) do
account_view_opts = Map.put(opts, :user, recipient)
if Map.has_key?(account_view_opts, :for) do
account_view_opts
else
Map.put(account_view_opts, :skip_visibility_check, true)
end
end
end

View file

@ -17,7 +17,7 @@ def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
%{
name: emoji,
count: length(users),
accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
accounts: render(AccountView, "index.json", users: users, for: user),
me: !!(user && user.ap_id in user_ap_ids)
}
end

Some files were not shown because too many files have changed in this diff Show more