Merge branch 'release/2.0.3' into 'stable'

Release/2.0.3

See merge request pleroma/secteam/pleroma!3
This commit is contained in:
rinpatch 2020-05-02 16:22:21 +00:00
commit 019a192e43
156 changed files with 1147 additions and 323 deletions

View file

@ -3,6 +3,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.0.3] - 2020-05-02
### Security
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
- CSP: Sandbox uploads
### Fixed
- Notifications from blocked domains
- Potential federation issues with Mastodon versions before 3.0.0
- HTTP Basic Authentication permissions issue
- Follow/Block imports not being able to find the user if the nickname started with an `@`
- Instance stats counting internal users
- Inability to run a From Source release without git
- ObjectAgePolicy didn't filter out old messages
- `blob:` urls not being allowed by CSP
### Added
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
- Follow request notifications
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
### Upgrade notes
1. Restart Pleroma
2. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
## [2.0.2] - 2020-04-08 ## [2.0.2] - 2020-04-08
### Added ### Added
- Support for Funkwhale's `Audio` activity - Support for Funkwhale's `Audio` activity

View file

@ -773,6 +773,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
### Restarts pleroma application ### Restarts pleroma application
**Only works when configuration from database is enabled.**
- Params: none - Params: none
- Response: - Response:
- On failure: - On failure:
@ -782,11 +784,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
{} {}
``` ```
## `GET /api/pleroma/admin/need_reboot`
### Returns the flag whether the pleroma should be restarted
- Params: none
- Response:
- `need_reboot` - boolean
```json
{
"need_reboot": false
}
```
## `GET /api/pleroma/admin/config` ## `GET /api/pleroma/admin/config`
### Get list of merged default settings with saved in database. ### Get list of merged default settings with saved in database.
*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.* *If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.** **Only works when configuration from database is enabled.**
@ -808,13 +823,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
"need_reboot": true "need_reboot": true
} }
``` ```
need_reboot - *optional*, if were changed reboot time settings.
## `POST /api/pleroma/admin/config` ## `POST /api/pleroma/admin/config`
### Update config settings ### Update config settings
*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.* *If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.** **Only works when configuration from database is enabled.**
@ -956,7 +970,6 @@ config :quack,
"need_reboot": true "need_reboot": true
} }
``` ```
need_reboot - *optional*, if were changed reboot time settings.
## ` GET /api/pleroma/admin/config/descriptions` ## ` GET /api/pleroma/admin/config/descriptions`

View file

@ -36,7 +36,7 @@ content-security-policy:
default-src 'none'; default-src 'none';
base-uri 'self'; base-uri 'self';
frame-ancestors 'none'; frame-ancestors 'none';
img-src 'self' data: https:; img-src 'self' data: blob: https:;
media-src 'self' https:; media-src 'self' https:;
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
font-src 'self'; font-src 'self';

View file

@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{ @mastodon_notification_types %{
"Create" => "mention", "Create" => "mention",
"Follow" => "follow", "Follow" => ["follow", "follow_request"],
"Announce" => "reblog", "Announce" => "reblog",
"Like" => "favourite", "Like" => "favourite",
"Move" => "move", "Move" => "move",
"EmojiReact" => "pleroma:emoji_reaction" "EmojiReact" => "pleroma:emoji_reaction"
} }
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -291,15 +287,43 @@ defp purge_web_resp_cache(%Activity{} = activity) do
defp purge_web_resp_cache(nil), do: nil defp purge_web_resp_cache(nil), do: nil
for {ap_type, type} <- @mastodon_notification_types do def follow_accepted?(
%Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
) do
with %User{} = follower <- Activity.user_actor(activity),
%User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
Pleroma.FollowingRelationship.following?(follower, followed)
else
_ -> false
end
end
def follow_accepted?(_), do: false
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}), def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type) do: unquote(type)
end end
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
if follow_accepted?(activity) do
"follow"
else
"follow_request"
end
end
def mastodon_notification_type(%Activity{}), do: nil def mastodon_notification_type(%Activity{}), do: nil
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
@doc "Converts Mastodon notification type to AR activity type"
def from_mastodon_notification_type(type) do def from_mastodon_notification_type(type) do
Map.get(@mastodon_to_ap_notification_types, type) with {k, _v} <-
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
k
end
end end
def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(actor, status_ids \\ [])

View file

@ -4,10 +4,16 @@
import EctoEnum import EctoEnum
defenum(UserRelationshipTypeEnum, defenum(Pleroma.UserRelationship.Type,
block: 1, block: 1,
mute: 2, mute: 2,
reblog_mute: 3, reblog_mute: 3,
notification_mute: 4, notification_mute: 4,
inverse_subscription: 5 inverse_subscription: 5
) )
defenum(Pleroma.FollowingRelationship.State,
follow_pending: 1,
follow_accept: 2,
follow_reject: 3
)

View file

@ -8,12 +8,13 @@ defmodule Pleroma.FollowingRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType alias FlakeId.Ecto.CompatType
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
schema "following_relationships" do schema "following_relationships" do
field(:state, :string, default: "accept") field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType) belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType) belongs_to(:following, User, type: CompatType)
@ -27,6 +28,18 @@ def changeset(%__MODULE__{} = following_relationship, attrs) do
|> put_assoc(:follower, attrs.follower) |> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following) |> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following]) |> validate_required([:state, :follower, :following])
|> unique_constraint(:follower_id,
name: :following_relationships_follower_id_following_id_index
)
|> validate_not_self_relationship()
end
def state_to_enum(state) when state in ["pending", "accept", "reject"] do
String.to_existing_atom("follow_#{state}")
end
def state_to_enum(state) do
raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
end end
def get(%User{} = follower, %User{} = following) do def get(%User{} = follower, %User{} = following) do
@ -35,7 +48,7 @@ def get(%User{} = follower, %User{} = following) do
|> Repo.one() |> Repo.one()
end end
def update(follower, following, "reject"), do: unfollow(follower, following) def update(follower, following, :follow_reject), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do case get(follower, following) do
@ -50,7 +63,7 @@ def update(%User{} = follower, %User{} = following, state) do
end end
end end
def follow(%User{} = follower, %User{} = following, state \\ "accept") do def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
%__MODULE__{} %__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state}) |> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing) |> Repo.insert(on_conflict: :nothing)
@ -80,7 +93,7 @@ def following_count(%User{} = user) do
def get_follow_requests(%User{id: id}) do def get_follow_requests(%User{id: id}) do
__MODULE__ __MODULE__
|> join(:inner, [r], f in assoc(r, :follower)) |> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == "pending") |> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id) |> where([r], r.following_id == ^id)
|> select([r, f], f) |> select([r, f], f)
|> Repo.all() |> Repo.all()
@ -88,7 +101,7 @@ def get_follow_requests(%User{id: id}) do
def following?(%User{id: follower_id}, %User{id: followed_id}) do def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__ __MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
|> Repo.exists?() |> Repo.exists?()
end end
@ -97,7 +110,7 @@ def following(%User{} = user) do
__MODULE__ __MODULE__
|> join(:inner, [r], u in User, on: r.following_id == u.id) |> join(:inner, [r], u in User, on: r.following_id == u.id)
|> where([r], r.follower_id == ^user.id) |> where([r], r.follower_id == ^user.id)
|> where([r], r.state == "accept") |> where([r], r.state == ^:follow_accept)
|> select([r, u], u.follower_address) |> select([r, u], u.follower_address)
|> Repo.all() |> Repo.all()
@ -129,4 +142,58 @@ def move_following(origin, target) do
move_following(origin, target) move_following(origin, target)
end end
end end
def all_between_user_sets(
source_users,
target_users
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
__MODULE__
|> where(
fragment(
"(follower_id = ANY(?) AND following_id = ANY(?)) OR \
(follower_id = ANY(?) AND following_id = ANY(?))",
^source_user_ids,
^target_user_ids,
^target_user_ids,
^source_user_ids
)
)
|> Repo.all()
end
def find(following_relationships, follower, following) do
Enum.find(following_relationships, fn
fr -> fr.follower_id == follower.id and fr.following_id == following.id
end)
end
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
|> validate_following_id_follower_id_inequality()
end
defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :follower_id, fn _, follower_id ->
if follower_id == get_field(changeset, :following_id) do
[source_id: "can't be equal to following_id"]
else
[]
end
end)
end
defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :following_id, fn _, following_id ->
if following_id == get_field(changeset, :follower_id) do
[target_id: "can't be equal to follower_id"]
else
[]
end
end)
end
end end

View file

@ -271,6 +271,16 @@ def destroy_multiple(%{id: user_id} = _user, ids) do
|> Repo.delete_all() |> Repo.delete_all()
end end
def dismiss(%Pleroma.Activity{} = activity) do
Notification
|> where([n], n.activity_id == ^activity.id)
|> Repo.delete_all()
|> case do
{_, notifications} -> {:ok, notifications}
_ -> {:error, "Cannot dismiss notification"}
end
end
def dismiss(%{id: user_id} = _user, id) do def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id) notification = Repo.get(Notification, id)
@ -294,7 +304,7 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
end end
def create_notifications(%Activity{data: %{"type" => type}} = activity) def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity) do_create_notifications(activity)
end end

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.Plugs.AuthExpectedPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _) do
put_private(conn, :auth_expected, true)
end
def auth_expected?(conn) do
conn.private[:auth_expected]
end
end

View file

@ -4,8 +4,11 @@
defmodule Pleroma.Plugs.AuthenticationPlug do defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
import Plug.Conn alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
import Plug.Conn
require Logger require Logger
def init(options), do: options def init(options), do: options
@ -37,6 +40,7 @@ def call(
if Pbkdf2.checkpw(password, password_hash) do if Pbkdf2.checkpw(password, password_hash) do
conn conn
|> assign(:user, auth_user) |> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
else else
conn conn
end end

View file

@ -75,7 +75,7 @@ defp csp_string do
"default-src 'none'", "default-src 'none'",
"base-uri 'self'", "base-uri 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
"img-src 'self' data: https:", "img-src 'self' data: blob: https:",
"media-src 'self' https:", "media-src 'self' https:",
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"font-src 'self'", "font-src 'self'",

View file

@ -4,6 +4,8 @@
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
import Plug.Conn import Plug.Conn
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
def init(options) do def init(options) do
@ -27,6 +29,7 @@ def call(
conn conn
|> assign(:auth_user, user) |> assign(:auth_user, user)
|> assign(:user, user) |> assign(:user, user)
|> OAuthScopesPlug.skip_plug()
else else
_ -> _ ->
conn conn

View file

@ -13,8 +13,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
def init(options), do: options def init(options), do: options
defp key_id_from_conn(conn) do defp key_id_from_conn(conn) do
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
Signature.key_id_to_actor_id(key_id) {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
ap_id
else else
_ -> _ ->
nil nil

View file

@ -8,12 +8,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.PlugHelper
use Pleroma.Web, :plug
@behaviour Plug @behaviour Plug
def init(%{scopes: _} = options), do: options def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :| op = options[:op] || :|
token = assigns[:token] token = assigns[:token]

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.PlugHelper do
@moduledoc "Pleroma Plug helper"
@called_plugs_list_id :called_plugs
def called_plugs_list_id, do: @called_plugs_list_id
@skipped_plugs_list_id :skipped_plugs
def skipped_plugs_list_id, do: @skipped_plugs_list_id
@doc "Returns `true` if specified plug was called."
def plug_called?(conn, plug_module) do
contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
end
@doc "Returns `true` if specified plug was explicitly marked as skipped."
def plug_skipped?(conn, plug_module) do
contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
end
@doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
def plug_called_or_skipped?(conn, plug_module) do
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
end
# Appends plug to known list (skipped, called). Intended to be used from within plug code only.
def append_to_private_list(conn, list_id, value) do
list = conn.private[list_id] || []
modified_list = Enum.uniq(list ++ [value])
Plug.Conn.put_private(conn, list_id, modified_list)
end
defp contained_in_private_list?(conn, private_variable, value) do
list = conn.private[private_variable] || []
value in list
end
end

View file

@ -41,6 +41,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn -> conn ->
conn conn
end end
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
config = Pleroma.Config.get(Pleroma.Upload) config = Pleroma.Config.get(Pleroma.Upload)

View file

@ -21,12 +21,21 @@ def key_id_to_actor_id(key_id) do
uri uri
end end
URI.to_string(uri) case uri do
%URI{scheme: scheme} when scheme in ["https", "http"] ->
{:ok, URI.to_string(uri)}
_ ->
case Pleroma.Web.WebFinger.finger(URI.to_string(uri)) do
%{"ap_id" => ap_id} -> {:ok, ap_id}
_ -> {:error, URI.to_string(uri)}
end
end
end end
def fetch_public_key(conn) do def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}
else else
@ -37,7 +46,7 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}

View file

@ -45,11 +45,11 @@ def get_peers do
end end
def init(_args) do def init(_args) do
{:ok, get_stat_data()} {:ok, calculate_stat_data()}
end end
def handle_call(:force_update, _from, _state) do def handle_call(:force_update, _from, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:reply, new_stats, new_stats} {:reply, new_stats, new_stats}
end end
@ -58,12 +58,12 @@ def handle_call(:get_state, _from, state) do
end end
def handle_cast(:run_update, _state) do def handle_cast(:run_update, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:noreply, new_stats} {:noreply, new_stats}
end end
defp get_stat_data do def calculate_stat_data do
peers = peers =
from( from(
u in User, u in User,
@ -77,7 +77,15 @@ defp get_stat_data do
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) users_query =
from(u in User,
where: u.deactivated != true,
where: u.local == true,
where: not is_nil(u.nickname),
where: not u.invisible
)
user_count = Repo.aggregate(users_query, :count, :id)
%{ %{
peers: peers, peers: peers,

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# A test controller reachable only in :test env.
# Serves to test OAuth scopes check skipping / enforcement.
defmodule Pleroma.Tests.OAuthTestController do
@moduledoc false
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
def skipped_oauth(conn, _params) do
noop(conn)
end
def performed_oauth(conn, _params) do
noop(conn)
end
def missed_oauth(conn, _params) do
noop(conn)
end
defp noop(conn), do: json(conn, %{})
end

View file

@ -227,6 +227,24 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \
end end
end end
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account" @doc "Returns status account"
@spec account_status(User.t()) :: account_status() @spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{deactivated: true}), do: :deactivated
@ -688,8 +706,10 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, "pending") follow(follower, followed, :follow_pending)
end end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@ -709,14 +729,14 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
def follow_all(follower, followeds) do def follow_all(follower, followeds) do
followeds followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, "accept")) |> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower) set_cache(follower)
end end
defdelegate following(user), to: FollowingRelationship defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ "accept") do def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
cond do cond do
@ -743,7 +763,7 @@ def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
def unfollow(%User{} = follower, %User{} = followed) do def unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do case get_follow_state(follower, followed) do
state when state in ["accept", "pending"] -> state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed) FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -761,14 +781,18 @@ def unfollow(%User{} = follower, %User{} = followed) do
defdelegate following?(follower, followed), to: FollowingRelationship defdelegate following?(follower, followed), to: FollowingRelationship
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following) following_relationship = FollowingRelationship.get(follower, following)
case {following_relationship, following.local} do case {following_relationship, following.local} do
{nil, false} -> {nil, false} ->
case Utils.fetch_latest_follow(follower, following) do case Utils.fetch_latest_follow(follower, following) do
%{data: %{"state" => state}} when state in ["pending", "accept"] -> state %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
_ -> nil FollowingRelationship.state_to_enum(state)
_ ->
nil
end end
{%{state: state}, _} -> {%{state: state}, _} ->
@ -1267,7 +1291,7 @@ def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) || blocks_user?(user, target) ||
(!User.following?(user, target) && blocks_domain?(user, target)) (blocks_domain?(user, target) and not User.following?(user, target))
end end
def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(%User{} = user, %User{} = target) do
@ -1416,9 +1440,16 @@ def perform(:delete, %User{} = user) do
end) end)
delete_user_activities(user) delete_user_activities(user)
if user.local do
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
else
invalidate_cache(user) invalidate_cache(user)
Repo.delete(user) Repo.delete(user)
end end
end
def perform(:deactivate_async, user, status), do: deactivate(user, status) def perform(:deactivate_async, user, status), do: deactivate(user, status)

View file

@ -148,7 +148,7 @@ defp compose_query({:followers, %User{id: id}}, query) do
as: :relationships, as: :relationships,
on: r.following_id == ^id and r.follower_id == u.id on: r.following_id == ^id and r.follower_id == u.id
) )
|> where([relationships: r], r.state == "accept") |> where([relationships: r], r.state == ^:follow_accept)
end end
defp compose_query({:friends, %User{id: id}}, query) do defp compose_query({:friends, %User{id: id}}, query) do
@ -158,7 +158,7 @@ defp compose_query({:friends, %User{id: id}}, query) do
as: :relationships, as: :relationships,
on: r.following_id == u.id and r.follower_id == ^id on: r.following_id == u.id and r.follower_id == ^id
) )
|> where([relationships: r], r.state == "accept") |> where([relationships: r], r.state == ^:follow_accept)
end end
defp compose_query({:recipients_from_activity, to}, query) do defp compose_query({:recipients_from_activity, to}, query) do
@ -173,7 +173,7 @@ defp compose_query({:recipients_from_activity, to}, query) do
) )
|> where( |> where(
[u, following: f, relationships: r], [u, following: f, relationships: r],
u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept") u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
) )
|> distinct(true) |> distinct(true)
end end

View file

@ -8,6 +8,8 @@ defmodule Pleroma.UserRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Ecto.Changeset
alias Pleroma.FollowingRelationship
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
@ -15,12 +17,12 @@ defmodule Pleroma.UserRelationship do
schema "user_relationships" do schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType) belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType) belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, UserRelationshipTypeEnum) field(:relationship_type, Pleroma.UserRelationship.Type)
timestamps(updated_at: false) timestamps(updated_at: false)
end end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do
# `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`, # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
# `def create_notification_mute/2`, `def create_inverse_subscription/2` # `def create_notification_mute/2`, `def create_inverse_subscription/2`
def unquote(:"create_#{relationship_type}")(source, target), def unquote(:"create_#{relationship_type}")(source, target),
@ -37,6 +39,10 @@ def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target) do: exists?(unquote(relationship_type), source, target)
end end
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship user_relationship
|> cast(params, [:relationship_type, :source_id, :target_id]) |> cast(params, [:relationship_type, :source_id, :target_id])
@ -75,16 +81,81 @@ def delete(relationship_type, %User{} = source, %User{} = target) do
end end
end end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do def dictionary(
changeset source_users,
|> validate_change(:target_id, fn _, target_id -> target_users,
if target_id == get_field(changeset, :source_id) do source_to_target_rel_types \\ nil,
[target_id: "can't be equal to source_id"] target_to_source_rel_types \\ nil
else )
[] when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end
source_to_target_rel_types =
Enum.map(source_to_target_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
target_to_source_rel_types =
Enum.map(target_to_source_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
__MODULE__
|> where(
fragment(
"(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?)) OR \
(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?))",
^source_user_ids,
^target_user_ids,
^source_to_target_rel_types,
^target_user_ids,
^source_user_ids,
^target_to_source_rel_types
)
)
|> select([ur], [ur.relationship_type, ur.source_id, ur.target_id])
|> Repo.all()
end end
end)
|> validate_change(:source_id, fn _, source_id -> def exists?(dictionary, rel_type, source, target, func) do
cond do
is_nil(source) or is_nil(target) ->
false
dictionary ->
[rel_type, source.id, target.id] in dictionary
true ->
func.(source, target)
end
end
@doc ":relationships option for StatusView / AccountView / NotificationView"
def view_relationships_option(nil = _reading_user, _actors) do
%{user_relationships: [], following_relationships: []}
end
def view_relationships_option(%User{} = reading_user, actors) do
user_relationships =
UserRelationship.dictionary(
[reading_user],
actors,
[:block, :mute, :notification_mute, :reblog_mute],
[:block, :inverse_subscription]
)
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
%{user_relationships: user_relationships, following_relationships: following_relationships}
end
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_source_id_target_id_inequality()
|> validate_target_id_source_id_inequality()
end
defp validate_source_id_target_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :source_id, fn _, source_id ->
if source_id == get_field(changeset, :target_id) do if source_id == get_field(changeset, :target_id) do
[source_id: "can't be equal to target_id"] [source_id: "can't be equal to target_id"]
else else
@ -92,4 +163,14 @@ defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
end end
end) end)
end end
defp validate_target_id_source_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :target_id, fn _, target_id ->
if target_id == get_field(changeset, :source_id) do
[target_id: "can't be equal to source_id"]
else
[]
end
end)
end
end end

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age" @moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp check_date(%{"published" => published} = message) do defp check_date(%{"object" => %{"published" => published}} = message) do
with %DateTime{} = now <- DateTime.utc_now(), with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published), {:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]), max_ttl <- Config.get([:mrf_object_age, :threshold]),
@ -96,5 +96,11 @@ def filter(%{"type" => "Create", "published" => _} = message) do
def filter(message), do: {:ok, message} def filter(message), do: {:ok, message}
@impl true @impl true
def describe, do: {:ok, %{}} def describe do
mrf_object_age =
Pleroma.Config.get(:mrf_object_age)
|> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}}
end
end end

View file

@ -491,7 +491,8 @@ def handle_incoming(
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <- {_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do {:ok, _relationship} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
actor: followed, actor: followed,
@ -501,7 +502,7 @@ def handle_incoming(
else else
{:user_blocked, true} -> {:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -512,7 +513,7 @@ def handle_incoming(
{:follow, {:error, _}} -> {:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -522,7 +523,7 @@ def handle_incoming(
}) })
{:user_locked, true} -> {:user_locked, true} ->
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
:noop :noop
end end
@ -542,7 +543,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{ ActivityPub.accept(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
type: "Accept", type: "Accept",
@ -565,7 +566,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, activity} <- {:ok, activity} <-
ActivityPub.reject(%{ ActivityPub.reject(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],

View file

@ -911,16 +911,7 @@ def config_show(conn, _params) do
end) end)
|> List.flatten() |> List.flatten()
response = %{configs: merged} json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
response =
if Restarter.Pleroma.need_reboot?() do
Map.put(response, :need_reboot, true)
else
response
end
json(conn, response)
end end
end end
@ -947,28 +938,22 @@ def config_update(conn, %{"configs" => configs}) do
Config.TransferTask.load_and_update_env(deleted, false) Config.TransferTask.load_and_update_env(deleted, false)
need_reboot? = if !Restarter.Pleroma.need_reboot?() do
Restarter.Pleroma.need_reboot?() || changed_reboot_settings? =
Enum.any?(updated, fn config -> (updated ++ deleted)
|> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group) group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key) key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value) value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value) Config.TransferTask.pleroma_need_restart?(group, key, value)
end) end)
response = %{configs: updated} if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
response =
if need_reboot? do
Restarter.Pleroma.need_reboot()
Map.put(response, :need_reboot, need_reboot?)
else
response
end end
conn conn
|> put_view(ConfigView) |> put_view(ConfigView)
|> render("index.json", response) |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end end
end end
@ -980,6 +965,10 @@ def restart(conn, _params) do
end end
end end
def need_reboot(conn, _params) do
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
end
defp configurable_from_database(conn) do defp configurable_from_database(conn) do
if Config.get(:configurable_from_database) do if Config.get(:configurable_from_database) do
:ok :ok

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
@ -39,10 +40,10 @@ def unfollow(follower, unfollowed) do
end end
def accept_follow_request(follower, followed) do def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
{:ok, _activity} <- {:ok, _activity} <-
ActivityPub.accept(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -57,7 +58,8 @@ def accept_follow_request(follower, followed) do
def reject_follow_request(follower, followed) do def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, _notifications} <- Notification.dismiss(follow_activity),
{:ok, _activity} <- {:ok, _activity} <-
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -83,8 +85,9 @@ def delete(activity_id, user) do
end end
end end
def repeat(id_or_ap_id, user, params \\ %{}) do def repeat(id, user, params \\ %{}) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity), object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object), announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do public <- public_announce?(object, params) do
@ -99,8 +102,9 @@ def repeat(id_or_ap_id, user, params \\ %{}) do
end end
end end
def unrepeat(id_or_ap_id, user) do def unrepeat(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unannounce(user, object) ActivityPub.unannounce(user, object)
else else
@ -109,8 +113,8 @@ def unrepeat(id_or_ap_id, user) do
end end
end end
def favorite(id_or_ap_id, user) do def favorite(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, with {_, %Activity{} = activity} <- {:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity), object <- Object.normalize(activity),
like_activity <- Utils.get_existing_like(user.ap_id, object) do like_activity <- Utils.get_existing_like(user.ap_id, object) do
if like_activity do if like_activity do
@ -124,8 +128,9 @@ def favorite(id_or_ap_id, user) do
end end
end end
def unfavorite(id_or_ap_id, user) do def unfavorite(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unlike(user, object) ActivityPub.unlike(user, object)
else else
@ -316,12 +321,12 @@ def update(user) do
}) })
end end
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{ with %Activity{
actor: ^user_ap_id, actor: ^user_ap_id,
data: %{"type" => "Create"}, data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}} object: %Object{data: %{"type" => object_type}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id), } = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"], true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do {:ok, _user} <- User.add_pinnned_activity(user, activity) do
@ -332,8 +337,8 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
end end
end end
def unpin(id_or_ap_id, user) do def unpin(id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity} {:ok, activity}
else else

View file

@ -24,24 +24,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
with true <- FlakeId.flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity
else
_ -> Activity.get_create_by_object_ap_id_with_object(id)
end
activity &&
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc) attachments_from_ids_descs(ids, desc)
end end

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastoFEController do
when action == :index when action == :index
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest])
@doc "GET /web/*path" @doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params) def index(%{assigns: %{user: user, token: token}} = conn, _params)

View file

@ -15,10 +15,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
@ -366,6 +369,8 @@ def blocks(%{assigns: %{user: user}} = conn, _) do
end end
@doc "GET /api/v1/endorsements" @doc "GET /api/v1/endorsements"
def endorsements(conn, params), def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end end

View file

@ -3,21 +3,31 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@moduledoc """
Contains stubs for unimplemented Mastodon API endpoints.
Note: instead of routing directly to this controller's action,
it's preferable to define an action in relevant (non-generic) controller,
set up OAuth rules for it and call this controller's function from it.
"""
use Pleroma.Web, :controller use Pleroma.Web, :controller
require Logger require Logger
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# Stubs for unimplemented mastodon api
#
def empty_array(conn, _) do def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array") Logger.debug("Unimplemented, returning an empty array (list)")
json(conn, []) json(conn, [])
end end
def empty_object(conn, _) do def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object") Logger.debug("Unimplemented, returning an empty object (map)")
json(conn, %{}) json(conn, %{})
end end
end end

View file

@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
require Logger require Logger
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
@doc "GET /api/v1/suggestions" @doc "GET /api/v1/suggestions"
def index(conn, _) do def index(conn, params),
json(conn, []) do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end
end end

View file

@ -49,12 +49,12 @@ def render("show.json", %{
"move" -> "move" ->
put_target(response, activity, user) put_target(response, activity, user)
"follow" ->
response
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
put_status(response, parent_activity, user) |> put_emoji(activity) put_status(response, parent_activity, user) |> put_emoji(activity)
type when type in ["follow", "follow_request"] ->
response
_ -> _ ->
nil nil
end end

View file

@ -27,6 +27,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_flash) plug(:fetch_flash)
plug(RateLimiter, [name: :authentication] when action == :create_authorization) plug(RateLimiter, [name: :authentication] when action == :create_authorization)
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug)
action_fallback(Pleroma.Web.OAuth.FallbackController) action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

View file

@ -34,7 +34,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action == :update_conversation %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
) )
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
@ -53,7 +53,10 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
else else
users = users =
Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
|> Enum.filter(& &1) |> Enum.filter(fn
%{deactivated: false} -> true
_ -> false
end)
%{ %{
name: emoji, name: emoji,

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
require Logger require Logger
import Ecto.Query import Ecto.Query
defdelegate mastodon_notification_type(activity), to: Activity
@types ["Create", "Follow", "Announce", "Like", "Move"] @types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions" @doc "Performs sending notifications for user subscriptions"
@ -24,32 +26,32 @@ def perform(
%{ %{
activity: %{data: %{"type" => activity_type}} = activity, activity: %{data: %{"type" => activity_type}} = activity,
user: %User{id: user_id} user: %User{id: user_id}
} = notif } = notification
) )
when activity_type in @types do when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
type = Activity.mastodon_notification_type(notif.activity) mastodon_type = mastodon_notification_type(notification.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity) object = Object.normalize(activity)
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user) direct_conversation_id = Activity.direct_conversation_id(activity, user)
for subscription <- fetch_subsriptions(user_id), for subscription <- fetch_subscriptions(user_id),
get_in(subscription.data, ["alerts", type]) do Subscription.enabled?(subscription, mastodon_type) do
%{ %{
access_token: subscription.token.token, access_token: subscription.token.token,
notification_id: notif.id, notification_id: notification.id,
notification_type: type, notification_type: mastodon_type,
icon: avatar_url, icon: avatar_url,
preferred_locale: "en", preferred_locale: "en",
pleroma: %{ pleroma: %{
activity_id: notif.activity.id, activity_id: notification.activity.id,
direct_conversation_id: direct_conversation_id direct_conversation_id: direct_conversation_id
} }
} }
|> Map.merge(build_content(notif, actor, object)) |> Map.merge(build_content(notification, actor, object, mastodon_type))
|> Jason.encode!() |> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription) |> push_message(build_sub(subscription), gcm_api_key, subscription)
end end
@ -82,7 +84,7 @@ def push_message(body, sub, api_key, subscription) do
end end
@doc "Gets user subscriptions" @doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do def fetch_subscriptions(user_id) do
Subscription Subscription
|> where(user_id: ^user_id) |> where(user_id: ^user_id)
|> preload(:token) |> preload(:token)
@ -99,28 +101,36 @@ def build_sub(subscription) do
} }
end end
def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content( def build_content(
%{ %{
activity: %{data: %{"directMessage" => true}}, activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}} user: %{notification_settings: %{privacy_option: true}}
}, },
actor, actor,
_ _object,
_mastodon_type
) do ) do
%{title: "New Direct Message", body: "@#{actor.nickname}"} %{title: "New Direct Message", body: "@#{actor.nickname}"}
end end
def build_content(notif, actor, object) do def build_content(notification, actor, object, mastodon_type) do
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
%{ %{
title: format_title(notif), title: format_title(notification, mastodon_type),
body: format_body(notif, actor, object) body: format_body(notification, actor, object, mastodon_type)
} }
end end
def format_body(activity, actor, object, mastodon_type \\ nil)
def format_body( def format_body(
%{activity: %{data: %{"type" => "Create"}}}, %{activity: %{data: %{"type" => "Create"}}},
actor, actor,
%{data: %{"content" => content}} %{data: %{"content" => content}},
_mastodon_type
) do ) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
@ -128,33 +138,44 @@ def format_body(
def format_body( def format_body(
%{activity: %{data: %{"type" => "Announce"}}}, %{activity: %{data: %{"type" => "Announce"}}},
actor, actor,
%{data: %{"content" => content}} %{data: %{"content" => content}},
_mastodon_type
) do ) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
def format_body( def format_body(
%{activity: %{data: %{"type" => type}}}, %{activity: %{data: %{"type" => type}}} = notification,
actor, actor,
_object _object,
mastodon_type
) )
when type in ["Follow", "Like"] do when type in ["Follow", "Like"] do
case type do mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post" case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
"follow_request" -> "@#{actor.nickname} has requested to follow you"
"favourite" -> "@#{actor.nickname} has favorited your post"
end end
end end
def format_title(%{activity: %{data: %{"directMessage" => true}}}) do def format_title(activity, mastodon_type \\ nil)
def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
"New Direct Message" "New Direct Message"
end end
def format_title(%{activity: %{data: %{"type" => type}}}) do def format_title(%{activity: activity}, mastodon_type) do
case type do mastodon_type = mastodon_type || mastodon_notification_type(activity)
"Create" -> "New Mention"
"Follow" -> "New Follower" case mastodon_type do
"Announce" -> "New Repeat" "mention" -> "New Mention"
"Like" -> "New Favorite" "follow" -> "New Follower"
"follow_request" -> "New Follow Request"
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
type -> "New #{String.capitalize(type || "event")}"
end end
end end
end end

View file

@ -32,6 +32,14 @@ defp alerts(%{"data" => %{"alerts" => alerts}}) do
%{"alerts" => alerts} %{"alerts" => alerts}
end end
def enabled?(subscription, "follow_request") do
enabled?(subscription, "follow")
end
def enabled?(subscription, alert_type) do
get_in(subscription.data, ["alerts", alert_type])
end
def create( def create(
%User{} = user, %User{} = user,
%Token{} = token, %Token{} = token,

View file

@ -64,5 +64,8 @@ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) d
def fetch_data_for_activity(_), do: %{} def fetch_data_for_activity(_), do: %{}
def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) def perform(:fetch, %Activity{} = activity) do
fetch_data_for_activity(activity)
:ok
end
end end

View file

@ -34,6 +34,7 @@ defmodule Pleroma.Web.Router do
pipeline :authenticated_api do pipeline :authenticated_api do
plug(:accepts, ["json"]) plug(:accepts, ["json"])
plug(:fetch_session) plug(:fetch_session)
plug(Pleroma.Plugs.AuthExpectedPlug)
plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug) plug(Pleroma.Plugs.UserFetcherPlug)
@ -199,6 +200,7 @@ defmodule Pleroma.Web.Router do
get("/config", AdminAPIController, :config_show) get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update) post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions) get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart) get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log) get("/moderation_log", AdminAPIController, :list_log)
@ -334,7 +336,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/relationships", AccountController, :relationships) get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists) get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/follow_requests", FollowRequestController, :index) get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks) get("/blocks", AccountController, :blocks)
@ -657,6 +659,17 @@ defmodule Pleroma.Web.Router do
end end
end end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
get("/#{action}", OAuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists) get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password) get("/check_password", MongooseIMController, :check_password)

View file

@ -197,27 +197,27 @@ def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
end end
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
with lines <- String.split(list, "\n"), followed_identifiers =
followed_identifiers <- list
Enum.map(lines, fn line -> |> String.split("\n")
String.split(line, ",") |> List.first() |> Enum.map(&(&1 |> String.split(",") |> List.first()))
end) |> List.delete("Account address")
|> List.delete("Account address") do |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
|> Enum.reject(&(&1 == ""))
User.follow_import(follower, followed_identifiers) User.follow_import(follower, followed_identifiers)
json(conn, "job started") json(conn, "job started")
end end
end
def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
blocks_import(conn, %{"list" => File.read!(listfile.path)}) blocks_import(conn, %{"list" => File.read!(listfile.path)})
end end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
with blocked_identifiers <- String.split(list) do blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@"))
User.blocks_import(blocker, blocked_identifiers) User.blocks_import(blocker, blocked_identifiers)
json(conn, "job started") json(conn, "job started")
end end
end
def change_password(%{assigns: %{user: user}} = conn, params) do def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do case CommonAPI.Utils.confirm_current_password(user, params["password"]) do

View file

@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(:errors) action_fallback(:errors)

View file

@ -29,11 +29,45 @@ def controller do
import Pleroma.Web.Router.Helpers import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
alias Pleroma.Plugs.PlugHelper
plug(:set_put_layout) plug(:set_put_layout)
defp set_put_layout(conn, _) do defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end end
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
defp skip_plug(conn, plug_module) do
try do
plug_module.skip_plug(conn)
rescue
UndefinedFunctionError ->
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
end
end
# Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do
with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
super(conn, params)
end
end
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
defp maybe_halt_on_missing_oauth_scopes_check(conn) do
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
"Security violation: OAuth scopes check was neither handled nor explicitly skipped."
)
|> halt()
else
conn
end
end
end end
end end
@ -96,6 +130,35 @@ def channel do
end end
end end
def plug do
quote do
alias Pleroma.Plugs.PlugHelper
@doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
"""
def skip_plug(conn) do
PlugHelper.append_to_private_list(
conn,
PlugHelper.skipped_plugs_list_id(),
__MODULE__
)
end
@impl Plug
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) do
conn
else
conn
|> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__)
|> perform(options)
end
end
end
end
@doc """ @doc """
When used, dispatch to the appropriate controller/view/etc. When used, dispatch to the appropriate controller/view/etc.
""" """

View file

@ -35,7 +35,7 @@ def perform(
_job _job
) do ) do
blocker = User.get_cached_by_id(blocker_id) blocker = User.get_cached_by_id(blocker_id)
User.perform(:blocks_import, blocker, blocked_identifiers) {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
end end
def perform( def perform(
@ -47,7 +47,7 @@ def perform(
_job _job
) do ) do
follower = User.get_cached_by_id(follower_id) follower = User.get_cached_by_id(follower_id)
User.perform(:follow_import, follower, followed_identifiers) {:ok, User.perform(:follow_import, follower, followed_identifiers)}
end end
def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do

23
mix.exs
View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do def project do
[ [
app: :pleroma, app: :pleroma,
version: version("2.0.2"), version: version("2.0.3"),
elixir: "~> 1.8", elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(), compilers: [:phoenix, :gettext] ++ Mix.compilers(),
@ -203,19 +203,26 @@ defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen # Pre-release version, denoted from patch version with a hyphen
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
{describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
{commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
git_pre_release = git_pre_release =
with {tag, 0} <- cond do
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true), tag_err == 0 and describe_err == 0 ->
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
describe describe
|> String.trim() |> String.trim()
|> String.replace(String.trim(tag), "") |> String.replace(String.trim(tag), "")
|> String.trim_leading("-") |> String.trim_leading("-")
|> String.trim() |> String.trim()
else
_ -> commit_hash_err == 0 ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
"0-g" <> String.trim(commit_hash) "0-g" <> String.trim(commit_hash)
true ->
""
end end
# Branch name as pre-release version component, denoted with a dot # Branch name as pre-release version component, denoted with a dot
@ -233,6 +240,8 @@ defp version(version) do
|> String.replace(identifier_filter, "-") |> String.replace(identifier_filter, "-")
branch_name branch_name
else
_ -> "stable"
end end
build_name = build_name =

View file

@ -0,0 +1,29 @@
defmodule Pleroma.Repo.Migrations.ChangeFollowingRelationshipsStateToInteger do
use Ecto.Migration
@alter_following_relationship_state "ALTER TABLE following_relationships ALTER COLUMN state"
def up do
execute("""
#{@alter_following_relationship_state} TYPE integer USING
CASE
WHEN state = 'pending' THEN 1
WHEN state = 'accept' THEN 2
WHEN state = 'reject' THEN 3
ELSE 0
END;
""")
end
def down do
execute("""
#{@alter_following_relationship_state} TYPE varchar(255) USING
CASE
WHEN state = 1 THEN 'pending'
WHEN state = 2 THEN 'accept'
WHEN state = 3 THEN 'reject'
ELSE ''
END;
""")
end
end

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddFollowingRelationshipsFollowingIdIndex do
use Ecto.Migration
# [:follower_index] index is useless because of [:follower_id, :following_id] index
# [:following_id] index makes sense because of user's followers-targeted queries
def change do
drop_if_exists(index(:following_relationships, [:follower_id]))
create_if_not_exists(index(:following_relationships, [:following_id]))
end
end

View file

@ -0,0 +1,45 @@
defmodule Pleroma.Repo.Migrations.InsertSkeletonsForDeletedUsers do
use Ecto.Migration
alias Pleroma.User
alias Pleroma.Repo
import Ecto.Query
def change do
Application.ensure_all_started(:flake_id)
local_ap_id =
User.Query.build(%{local: true})
|> select([u], u.ap_id)
|> limit(1)
|> Repo.one()
unless local_ap_id == nil do
# Hack to get instance base url because getting it from Phoenix
# would require starting the whole application
instance_uri =
local_ap_id
|> URI.parse()
|> Map.put(:query, nil)
|> Map.put(:path, nil)
|> URI.to_string()
{:ok, %{rows: ap_ids}} =
Ecto.Adapters.SQL.query(
Repo,
"select distinct unnest(nonexistent_locals.recipients) from activities, lateral (select array_agg(recipient) as recipients from unnest(activities.recipients) as recipient where recipient similar to '#{
instance_uri
}/users/[A-Za-z0-9]*' and not(recipient in (select ap_id from users where local = true))) nonexistent_locals;",
[],
timeout: :infinity
)
ap_ids
|> Enum.each(fn [ap_id] ->
Ecto.Changeset.change(%User{}, deactivated: true, ap_id: ap_id)
|> Repo.insert()
end)
end
end
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.1abbc9b8.css rel=stylesheet><link href=chunk-libs.686b5876.css rel=stylesheet><link href=app.85534e14.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.cb26bbd1.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.d898cc2b.js></script></body></html> <!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.1abbc9b8.css rel=stylesheet><link href=chunk-libs.686b5876.css rel=stylesheet><link href=app.796ca6d4.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.1b4f6ce0.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.203f69f8.js></script></body></html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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