...
 
Commits (150)
......@@ -15,16 +15,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- Configuration: Add `:database_config_whitelist` setting to whitelist settings which can be configured from AdminFE.
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
<details>
<summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.
......@@ -41,12 +44,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP
## [Unreleased (patch)]
### Fixed
- Healthcheck reporting the number of memory currently used, rather than allocated in total
- `InsertSkeletonsForDeletedUsers` failing on some instances
- `InsertSkeletonsForDeletedUsers` failing on some instances
## [2.0.3] - 2020-05-02
......
......@@ -71,7 +71,8 @@
follow_redirect: true,
pool: :upload
]
]
],
filename_display_max_length: 30
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
......@@ -273,7 +274,7 @@
config :pleroma, :frontend_configurations,
pleroma_fe: %{
alwaysShowSubjectInput: true,
background: "/static/aurora_borealis.jpg",
background: "/images/city.jpg",
collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
......
......@@ -119,6 +119,11 @@
]
}
]
},
%{
key: :filename_display_max_length,
type: :integer,
description: "Set max length of a filename to display. 0 = no limit. Default: 30"
}
]
},
......@@ -969,6 +974,13 @@
]
}
]
},
%{
key: :instance_thumbnail,
type: :string,
description:
"The instance thumbnail image. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html)",
suggestions: ["/instance/thumbnail.jpeg"]
}
]
},
......@@ -1112,7 +1124,7 @@
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "",
nsfwCensorImage: "/static/img/nsfw.74818f9.png",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
......@@ -1226,7 +1238,7 @@
type: :string,
description:
"URL of the image to use for hiding NSFW media attachments in the timeline.",
suggestions: ["/static/img/nsfw.png"]
suggestions: ["/static/img/nsfw.74818f9.png"]
},
%{
key: :postContentType,
......@@ -1346,6 +1358,12 @@
suggestions: [
:pleroma_fox_tan
]
},
%{
key: :default_user_avatar,
type: :string,
description: "URL of the default user avatar.",
suggestions: ["/images/avi.png"]
}
]
},
......
......@@ -511,7 +511,23 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `discoverable`
- `actor_type`
- Response: none (code `200`)
- Response:
```json
{"status": "success"}
```
```json
{"errors":
{"actor_type": "is invalid"},
{"email": "has invalid format"},
...
}
```
```json
{"error": "Unable to update user."}
```
## `GET /api/pleroma/admin/reports`
......
......@@ -220,3 +220,7 @@ There's a new `pleroma:chat_mention` notification, which has this form:
"created_at": "somedate"
}
```
### Streaming
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
......@@ -6,10 +6,6 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
## Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
......@@ -32,12 +28,20 @@ Has these additional fields under the `pleroma` object:
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
## Attachments
## Media Attachments
Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment.
### Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
### Limitations
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
## Accounts
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
......@@ -226,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications
## Streaming
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
......@@ -358,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
## `GET /api/v1/pleroma/conversations/read`
## `POST /api/v1/pleroma/conversations/read`
### Marks all user's conversations as read.
* Method `POST`
* Authentication: required
......@@ -536,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
```
## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
* Method: `GET`
* Authentication: optional
* Params: None
......
......@@ -69,3 +69,32 @@ mix pleroma.database update_users_following_followers_counts
```sh tab="From Source"
mix pleroma.database fix_likes_collections
```
## Vacuum the database
### Analyze
Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.**
```sh tab="OTP"
./bin/pleroma_ctl database vacuum analyze
```
```sh tab="From Source"
mix pleroma.database vacuum analyze
```
### Full
Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller
and more compact files with an optimized layout. This process will take a long time and use additional disk space as
it builds the files side-by-side the existing database files. It can make your database faster and use less disk space,
but should only be run if necessary. **It is safe to cancel this.**
```sh tab="OTP"
./bin/pleroma_ctl database vacuum full
```
```sh tab="From Source"
mix pleroma.database vacuum full
```
\ No newline at end of file
......@@ -95,33 +95,33 @@ mix pleroma.user sign_out <nickname>
```
## Deactivate or activate a user
## Deactivate or activate a user
```sh tab="OTP"
./bin/pleroma_ctl user toggle_activated <nickname>
./bin/pleroma_ctl user toggle_activated <nickname>
```
```sh tab="From Source"
mix pleroma.user toggle_activated <nickname>
mix pleroma.user toggle_activated <nickname>
```
## Unsubscribe local users from a user and deactivate the user
## Deactivate a user and unsubscribes local users from the user
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe NICKNAME
./bin/pleroma_ctl user deactivate NICKNAME
```
```sh tab="From Source"
mix pleroma.user unsubscribe NICKNAME
mix pleroma.user deactivate NICKNAME
```
## Unsubscribe local users from an instance and deactivate all accounts on it
## Deactivate all accounts from an instance and unsubscribe local users on it
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
./bin/pleroma_ctl user deactivate_all_from_instance <instance>
```
```sh tab="From Source"
mix pleroma.user unsubscribe_all_from_instance <instance>
mix pleroma.user deactivate_all_from_instance <instance>
```
......@@ -177,4 +177,3 @@ mix pleroma.user untag <nickname> <tags>
```sh tab="From Source"
mix pleroma.user toggle_confirmed <nickname>
```
......@@ -149,6 +149,11 @@ config :pleroma, :mrf_user_allowlist,
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely
#### mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
......@@ -493,6 +498,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
!!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
......
# Optimizing your PostgreSQL performance
Pleroma performance depends to a large extent on good database performance. The default PostgreSQL settings are mostly fine, but often you can get better performance by changing a few settings.
You can use [PGTune](https://pgtune.leopard.in.ua) to get recommendations for your setup. If you do, set the "Number of Connections" field to 20, as Pleroma will only use 10 concurrent connections anyway. If you don't, it will give you advice that might even hurt your performance.
We also recommend not using the "Network Storage" option.
## Example configurations
Here are some configuration suggestions for PostgreSQL 10+.
### 1GB RAM, 1 CPU
```
shared_buffers = 256MB
effective_cache_size = 768MB
maintenance_work_mem = 64MB
work_mem = 13107kB
```
### 2GB RAM, 2 CPU
```
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
work_mem = 26214kB
max_worker_processes = 2
max_parallel_workers_per_gather = 1
max_parallel_workers = 2
```
......@@ -38,8 +38,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
* Download and add the Erlang repository:
```shell
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
```
* Install Elixir and Erlang:
......
......@@ -40,8 +40,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
* Erlangのリポジトリをダウンロードおよびインストールします。
```
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
```
* ElixirとErlangをインストールします、
......
......@@ -63,7 +63,7 @@ apt install postgresql-11-rum
```
#### (Optional) Performance configuration
For optimal performance, you may use [PGTune](https://pgtune.leopard.in.ua), don't forget to restart postgresql after editing the configuration
It is encouraged to check [Optimizing your PostgreSQL performance](../configuration/postgresql.md) document, for tips on PostgreSQL tuning.
```sh tab="Alpine"
rc-service postgresql restart
......
......@@ -4,6 +4,7 @@
defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.Conversation
alias Pleroma.Maintenance
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
......@@ -34,13 +35,7 @@ def run(["remove_embedded_objects" | args]) do
)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
Maintenance.vacuum("full")
end
end
......@@ -94,13 +89,7 @@ def run(["prune_objects" | args]) do
|> Repo.delete_all(timeout: :infinity)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
Maintenance.vacuum("full")
end
end
......@@ -135,4 +124,10 @@ def run(["fix_likes_collections"]) do
end)
|> Stream.run()
end
def run(["vacuum", args]) do
start_pleroma()
Maintenance.vacuum(args)
end
end
......@@ -15,7 +15,7 @@ def run(["ls-packs" | args]) do
{options, [], []} = parse_global_opts(args)
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(url_or_path)
manifest = fetch_and_decode(url_or_path)
Enum.each(manifest, fn {name, info} ->
to_print = [
......@@ -42,12 +42,12 @@ def run(["get-packs" | args]) do
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(url_or_path)
manifest = fetch_and_decode(url_or_path)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name]
src_url = pack["src"]
src = pack["src"]
IO.puts(
IO.ANSI.format([
......@@ -57,11 +57,11 @@ def run(["get-packs" | args]) do
:normal,
" from ",
:underline,
src_url
src
])
)
binary_archive = Tesla.get!(client(), src_url).body
{:ok, binary_archive} = fetch(src)
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
......@@ -74,8 +74,8 @@ def run(["get-packs" | args]) do
raise "Bad SHA256 for #{pack_name}"
end
# The url specified in files should be in the same directory
files_url =
# The location specified in files should be in the same directory
files_loc =
url_or_path
|> Path.dirname()
|> Path.join(pack["files"])
......@@ -88,11 +88,11 @@ def run(["get-packs" | args]) do
:normal,
" from ",
:underline,
files_url
files_loc
])
)
files = Tesla.get!(client(), files_url).body |> Jason.decode!()
files = fetch_and_decode(files_loc)
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
......@@ -237,16 +237,20 @@ def run(["gen-pack" | args]) do
end
end
defp fetch_manifest(from) do
Jason.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(client(), from).body
else
File.read!(from)
end
)
defp fetch_and_decode(from) do
with {:ok, json} <- fetch(from) do
Jason.decode!(json)
end
end
defp fetch("http" <> _ = from) do
with {:ok, %{body: body}} <- Tesla.get(client(), from) do
{:ok, body}
end
end
defp fetch(path), do: File.read(path)
defp parse_global_opts(args) do
OptionParser.parse(
args,
......
......@@ -147,6 +147,7 @@ def run(["gen" | rest]) do
"What directory should media uploads go in (when using the local uploader)?",
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
)
|> Path.expand()
static_dir =
get_option(
......@@ -155,6 +156,7 @@ def run(["gen" | rest]) do
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Pleroma.Config.get([:instance, :static_dir])
)
|> Path.expand()
Config.put([:instance, :static_dir], static_dir)
......@@ -204,7 +206,7 @@ def run(["gen" | rest]) do
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(indexable, template_dir)
write_robots_txt(static_dir, indexable, template_dir)
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
......@@ -224,15 +226,13 @@ def run(["gen" | rest]) do
end
end
defp write_robots_txt(indexable, template_dir) do
defp write_robots_txt(static_dir, indexable, template_dir) do
robots_txt =
EEx.eval_file(
template_dir <> "/robots_txt.eex",
indexable: indexable
)
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
unless File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end
......
......@@ -144,28 +144,18 @@ def run(["reset_password", nickname]) do
end
end
def run(["unsubscribe", nickname]) do
def run(["deactivate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)
user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(User.get_friends(user)) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
end
else
_ ->
......@@ -173,7 +163,7 @@ def run(["unsubscribe", nickname]) do
end
end
def run(["unsubscribe_all_from_instance", instance]) do
def run(["deactivate_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
......@@ -181,7 +171,7 @@ def run(["unsubscribe_all_from_instance", instance]) do
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["unsubscribe", user.nickname])
run(["deactivate", user.nickname])
end)
end)
|> Stream.run()
......
......@@ -24,10 +24,7 @@ def by_ap_id(query \\ Activity, ap_id) do
@spec by_actor(query, String.t()) :: query
def by_actor(query \\ Activity, actor) do
from(
activity in query,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor)
)
from(a in query, where: a.actor == ^actor)
end
@spec by_author(query, User.t()) :: query
......
......@@ -63,7 +63,7 @@ def create_or_bump_for(activity, opts \\ []) do
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
users = User.get_users_from_set(activity.recipients, false)
users = User.get_users_from_set(activity.recipients, local_only: false)
participations =
Enum.map(users, fn user ->
......
......@@ -14,8 +14,10 @@ def new_users(to, users_and_statuses) do
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
logo_url =
Pleroma.Web.Endpoint.url() <>
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo])
Pleroma.Helpers.UriHelper.maybe_add_base(
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo]),
Pleroma.Web.Endpoint.url()
)
new()
|> to({to.name, to.email})
......
......@@ -499,7 +499,7 @@ defp download_archive(url, sha) do
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
{:ok, archive}
else
{:error, :imvalid_checksum}
{:error, :invalid_checksum}
end
end
end
......
......@@ -24,4 +24,7 @@ def append_param_if_present(%{} = params, param_name, param_value) do
params
end
end
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
def maybe_add_base(uri, _base), do: uri
end
......@@ -22,22 +22,7 @@ def options(connection_opts \\ [], %URI{} = uri) do
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
end
defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
ssl_opts = [
ssl_options: [
# Workaround for remote server certificate chain issues
partial_chain: &:hackney_connect.partial_chain/1,
# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
server_name_indication: to_charlist(host)
]
]
Keyword.merge(opts, ssl_opts)
end
defp add_scheme_opts(opts, _), do: opts
def after_request(_), do: :ok
end
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maintenance do
alias Pleroma.Repo
require Logger
def vacuum(args) do
case args do
"analyze" ->
Logger.info("Runnning VACUUM ANALYZE.")
Repo.query!(
"vacuum analyze;",
[],
timeout: :infinity
)
"full" ->
Logger.info("Runnning VACUUM FULL.")
Logger.warn(
"Re-packing your entire database may take a while and will consume extra disk space during the process."
)
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
_ ->
Logger.error("Error: invalid vacuum argument.")
end
end
end
......@@ -92,8 +92,9 @@ def for_user_query(user, opts \\ %{}) do
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
object.data,
a.data,
a.data
)
)
......@@ -224,18 +225,8 @@ def set_read_up_to(%{id: user_id} = user, id) do
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
Notification
for_user_query(user)
|> where([n], n.id in ^notification_ids)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
|> Repo.all()
end
......@@ -370,7 +361,8 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
potential_receivers =
User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
notification_enabled_ap_ids =
potential_receiver_ap_ids
......
......@@ -31,7 +31,7 @@ defp headers do
{"x-content-type-options", "nosniff"},
{"referrer-policy", referrer_policy},
{"x-download-options", "noopen"},
{"content-security-policy", csp_string() <> ";"}
{"content-security-policy", csp_string()}
]
if report_uri do
......@@ -43,23 +43,46 @@ defp headers do
]
}
headers ++ [{"reply-to", Jason.encode!(report_group)}]
[{"reply-to", Jason.encode!(report_group)} | headers]
else
headers
end
end
static_csp_rules = [
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
"manifest-src 'self'"
]
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = Pleroma.Web.Endpoint.websocket_url()
report_uri = Config.get([:http_security, :report_uri])
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
{img_src, media_src} =
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
sources = get_proxy_and_attachment_sources()
{[img_src, sources], [media_src, sources]}
else
{[img_src, " https:"], [media_src, " https:"]}
end
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
connect_src =
if Pleroma.Config.get(:env) == :dev do
connect_src <> " http://localhost:3035/"
[connect_src, " http://localhost:3035/"]
else
connect_src
end
......@@ -71,27 +94,46 @@ defp csp_string do
"script-src 'self'"
end
main_part = [
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"img-src 'self' data: blob: https:",
"media-src 'self' https:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
"manifest-src 'self'",
connect_src,
script_src
]
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
insecure = if scheme == "https", do: "upgrade-insecure-requests"
@csp_start
|> add_csp_param(img_src)
|> add_csp_param(media_src)
|> add_csp_param(connect_src)
|> add_csp_param(script_src)
|> add_csp_param(insecure)
|> add_csp_param(report)
|> :erlang.iolist_to_binary()
end
defp get_proxy_and_attachment_sources do
media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host)
end)
report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
upload_base_url =
if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
s3_endpoint =
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
(main_part ++ report ++ insecure)
|> Enum.join("; ")
[]
|> add_source(upload_base_url)
|> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist)
end
defp add_source(iodata, nil), do: iodata
defp add_source(iodata, source), do: [[?\s, source] | iodata]
defp add_csp_param(csp_iodata, nil), do: csp_iodata
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do
Logger.warn("
......
......@@ -67,6 +67,7 @@ def store(upload, opts \\ []) do
{:ok,
%{
"type" => opts.activity_type,
"mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
......
......@@ -305,8 +305,13 @@ def invisible?(_), do: false
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
%{"url" => [%{"href" => href} | _]} ->
href
_ ->
unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
end
end
end
......@@ -533,9 +538,10 @@ def update_as_admin_changeset(struct, params) do
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
......@@ -556,7 +562,7 @@ def password_update_changeset(struct, params) do
|> put_change(:password_reset_pending, false)
end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
......@@ -749,7 +755,19 @@ def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"}
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
error ->
error
end
end
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
......@@ -760,7 +778,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
|> update_following_count()
|> set_cache()
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
{:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
......@@ -1191,8 +1209,9 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user)
def increment_unread_conversation_count(_, user), do: {:ok, user}
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
......@@ -1402,15 +1421,13 @@ def deactivate(%User{} = user, status) do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(fn follower ->
follower |> update_following_count() |> set_cache()
end)
|> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&update_follower_count/1)
|> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
......@@ -1603,12 +1620,19 @@ def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id)
cached_user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !needs_update?(user) do
{:ok, user}
else
fetch_by_ap_id(ap_id)
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
case {cached_user, maybe_fetched_user} do
{_, {:ok, %User{} = user}} ->
{:ok, user}
{%User{} = user, _} ->
{:ok, user}
_ ->
{:error, :not_found}
end
end
......
......@@ -363,36 +363,6 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end
end
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with {:ok, result} <-
Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
result
end
end
defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public),
object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
false -> {:error, false}
{:error, error} -> Repo.rollback(error)
end
end
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def follow(follower, followed, activity_id \\ nil, local \\ true) do
......@@ -575,14 +545,27 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|> Repo.one()
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])
[Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination)
query = fetch_activities_query([Constants.as_public()], opts)
query =
if opts["restrict_unlisted"] do
restrict_unlisted(query)
else
query
end
Pagination.fetch_paginated(query, opts, pagination)
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts
|> Map.put("restrict_unlisted", true)
|> fetch_public_or_unlisted_activities(pagination)
end
@valid_visibilities ~w[direct unlisted public private]
......@@ -1195,7 +1178,7 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([_like, object, activity], %{activity | object: object})
|> order_by([like, _, _], desc: like.id)
|> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{"skip_order" => true}),
pagination,
......
......@@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator
......@@ -75,8 +76,8 @@ def user(conn, %{"nickname" => nickname}) do
end
end
def object(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
def object(conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
......@@ -101,8 +102,8 @@ def track_object_fetch(conn, object_id) do
conn
end
def activity(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid),
def activity(conn, _params) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
......
......@@ -8,9 +8,12 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
require Pleroma.Constants
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
......@@ -120,6 +123,34 @@ def like(actor, object) do
end
end
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false)
to =
cond do
actor.ap_id == Relay.relay_ap_id() ->
[actor.follower_address]
public? ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
true ->
[actor.follower_address, object.data["actor"]]
end
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"object" => object.data["id"],
"to" => to,
"context" => object.data["context"],
"type" => "Announce",
"published" => Utils.make_date()
}, []}
end
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
defp object_action(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
......
# 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.MRF.StealEmojiPolicy do
require Logger
alias Pleroma.Config
@moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF
defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
defp steal_emoji({shortcode, url}) do
url = Pleroma.Web.MediaProxy.url(url)
{:ok, response} = Pleroma.HTTP.get(url)
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
if byte_size(response.body) <= size_limit do
emoji_dir_path =
Config.get(
[:mrf_steal_emoji, :path],
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
extension =
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()
file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
try do
:ok = File.write(file_path, response.body)
shortcode
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end
else
Logger.debug(
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
size_limit
} B)"
)
nil
end
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
nil
end
@impl true
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
host = URI.parse(actor).host
if remote_host?(host) and accept_host?(host) do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
new_emojis =
foreign_emojis
|> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
|> Enum.filter(fn {shortcode, _url} ->
reject_emoji? =
Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
!reject_emoji?
end)
|> Enum.map(&steal_emoji(&1))
|> Enum.filter(& &1)
if !Enum.empty?(new_emojis) do
Logger.info("Stole new emojis: #{inspect(new_emojis)}")
Pleroma.Emoji.reload()
end
end
{:ok, message}
end
def filter(message), do: {:ok, message}
@impl true
def describe do
{:ok, %{}}
end
end
......@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
......@@ -84,6 +85,16 @@ def validate(%{"type" => "Create", "object" => object} = 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())
{:ok, object, meta}
end
end
def cast_and_apply(%{"type" => "ChatMessage"} = object) do