Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop

This commit is contained in:
sadposter 2019-09-26 23:22:24 +01:00
commit 1e11b97c1b
59 changed files with 1335 additions and 1381 deletions

View file

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
### Fixed ### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
@ -108,6 +109,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Added moderation log - Admin API: Added moderation log
- Web response cache (currently, enabled for ActivityPub) - Web response cache (currently, enabled for ActivityPub)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`) - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
### Changed ### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
@ -115,6 +118,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: parsers and their order are configured in `rich_media` config. - RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time. - RichMedia: add the rich media ttl based on image expiration time.
## [1.0.7] - 2019-09-26
### Fixed
- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
### Changed
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
## [1.0.6] - 2019-08-14 ## [1.0.6] - 2019-08-14
### Fixed ### Fixed
- MRF: fix use of unserializable keyword lists in describe() implementations - MRF: fix use of unserializable keyword lists in describe() implementations

View file

@ -308,7 +308,15 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `GET` - Methods: `GET`
- Params: none - Params: none
- Response: password reset token (base64 string) - Response:
```json
{
"token": "U13DX6muOvpRsj35_ij9wLxUbkU-eFvfKttxs6gIajo=", // password reset token (base64 string)
"link": "https://pleroma.social/api/pleroma/password_reset/U13DX6muOvpRsj35_ij9wLxUbkU-eFvfKttxs6gIajo%3D"
}
```
## `/api/pleroma/admin/users/:nickname/force_password_reset` ## `/api/pleroma/admin/users/:nickname/force_password_reset`
@ -723,7 +731,11 @@ Compile time settings (need instance reboot):
- Method `GET` - Method `GET`
- Params: - Params:
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`) - *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42`
- *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42
- *optional* `user_id`: **integer** filter logs by actor's id
- *optional* `search`: **string** search logs by the log message
- Response: - Response:
```json ```json

View file

@ -4,7 +4,6 @@
defmodule Mix.Tasks.Pleroma.User do defmodule Mix.Tasks.Pleroma.User do
use Mix.Task use Mix.Task
import Ecto.Changeset
import Mix.Pleroma import Mix.Pleroma
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
@ -228,9 +227,9 @@ def run(["unsubscribe", nickname]) do
shell_info("Deactivating #{user.nickname}") shell_info("Deactivating #{user.nickname}")
User.deactivate(user) User.deactivate(user)
{:ok, friends} = User.get_friends(user) user
|> User.get_friends()
Enum.each(friends, fn friend -> |> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}") shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
@ -405,7 +404,7 @@ def run(["delete_activities", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, _} = User.delete_user_activities(user) User.delete_user_activities(user)
shell_info("User #{nickname} statuses deleted.") shell_info("User #{nickname} statuses deleted.")
else else
_ -> _ ->
@ -472,39 +471,21 @@ def run(["sign_out", nickname]) do
end end
defp set_moderator(user, value) do defp set_moderator(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_moderator: value}))
user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
{:ok, user} = User.update_and_set_cache(user_cng)
shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
user user
end end
defp set_admin(user, value) do defp set_admin(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_admin: value}) {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_admin: value}))
user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
{:ok, user} = User.update_and_set_cache(user_cng)
shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}") shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
user user
end end
defp set_locked(user, value) do defp set_locked(user, value) do
info_cng = User.Info.user_upgrade(user.info, %{locked: value}) {:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: value}))
user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
{:ok, user} = User.update_and_set_cache(user_cng)
shell_info("Locked status of #{user.nickname}: #{user.info.locked}") shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
user user

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Activity do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@type actor :: String.t() @type actor :: String.t()
@primary_key {:id, Pleroma.FlakeId, autogenerate: true} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# 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 %{

View file

@ -7,7 +7,6 @@ defmodule Pleroma.ActivityExpiration do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration alias Pleroma.ActivityExpiration
alias Pleroma.FlakeId
alias Pleroma.Repo alias Pleroma.Repo
import Ecto.Changeset import Ecto.Changeset
@ -17,7 +16,7 @@ defmodule Pleroma.ActivityExpiration do
@min_activity_lifetime :timer.hours(1) @min_activity_lifetime :timer.hours(1)
schema "activity_expirations" do schema "activity_expirations" do
belongs_to(:activity, Activity, type: FlakeId) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime) field(:scheduled_at, :naive_datetime)
end end

View file

@ -35,7 +35,6 @@ def start(_type, _args) do
Pleroma.Config.TransferTask, Pleroma.Config.TransferTask,
Pleroma.Emoji, Pleroma.Emoji,
Pleroma.Captcha, Pleroma.Captcha,
Pleroma.FlakeId,
Pleroma.Daemons.ScheduledActivityDaemon, Pleroma.Daemons.ScheduledActivityDaemon,
Pleroma.Daemons.ActivityExpirationDaemon Pleroma.Daemons.ActivityExpirationDaemon
] ++ ] ++

View file

@ -10,20 +10,20 @@ defmodule Pleroma.Bookmark do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark alias Pleroma.Bookmark
alias Pleroma.FlakeId
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "bookmarks" do schema "bookmarks" do
belongs_to(:user, User, type: FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps() timestamps()
end end
@spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do def create(user_id, activity_id) do
attrs = %{ attrs = %{
user_id: user_id, user_id: user_id,
@ -37,7 +37,7 @@ def create(user_id, activity_id) do
|> Repo.insert() |> Repo.insert()
end end
@spec for_user_query(FlakeId.t()) :: Ecto.Query.t() @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
def for_user_query(user_id) do def for_user_query(user_id) do
Bookmark Bookmark
|> where(user_id: ^user_id) |> where(user_id: ^user_id)
@ -52,7 +52,8 @@ def get(user_id, activity_id) do
|> Repo.one() |> Repo.one()
end end
@spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} @spec destroy(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
def destroy(user_id, activity_id) do def destroy(user_id, activity_id) do
from(b in Bookmark, from(b in Bookmark,
where: b.user_id == ^user_id, where: b.user_id == ^user_id,

View file

@ -13,10 +13,10 @@ defmodule Pleroma.Conversation.Participation do
import Ecto.Query import Ecto.Query
schema "conversation_participations" do schema "conversation_participations" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:conversation, Conversation) belongs_to(:conversation, Conversation)
field(:read, :boolean, default: false) field(:read, :boolean, default: false)
field(:last_activity_id, Pleroma.FlakeId, virtual: true) field(:last_activity_id, FlakeId.Ecto.CompatType, virtual: true)
has_many(:recipient_ships, RecipientShip) has_many(:recipient_ships, RecipientShip)
has_many(:recipients, through: [:recipient_ships, :user]) has_many(:recipients, through: [:recipient_ships, :user])

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Conversation.Participation.RecipientShip do
import Ecto.Changeset import Ecto.Changeset
schema "conversation_participation_recipient_ships" do schema "conversation_participation_recipient_ships" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:participation, Participation) belongs_to(:participation, Participation)
end end

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Delivery do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.FlakeId
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -16,7 +15,7 @@ defmodule Pleroma.Delivery do
import Ecto.Query import Ecto.Query
schema "deliveries" do schema "deliveries" do
belongs_to(:user, User, type: FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:object, Object) belongs_to(:object, Object)
end end

View file

@ -99,7 +99,7 @@ defp load_pack(pack_dir, emoji_groups) do
contents["files"] contents["files"]
|> Enum.map(fn {name, rel_file} -> |> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file) filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name} {name, filename, ["pack:#{pack_name}"]}
end) end)
else else
# Load from emoji.txt / all files # Load from emoji.txt / all files

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Filter do
alias Pleroma.User alias Pleroma.User
schema "filters" do schema "filters" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:filter_id, :integer) field(:filter_id, :integer)
field(:hide, :boolean, default: false) field(:hide, :boolean, default: false)
field(:whole_word, :boolean, default: true) field(:whole_word, :boolean, default: true)

View file

@ -1,182 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FlakeId do
@moduledoc """
Flake is a decentralized, k-ordered id generation service.
Adapted from:
* [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
* [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
"""
@type t :: binary
use Ecto.Type
use GenServer
require Logger
alias __MODULE__
import Kernel, except: [to_string: 1]
defstruct node: nil, time: 0, sq: 0
@doc "Converts a binary Flake to a String"
def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
Kernel.to_string(id)
end
def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do
encode_base62(flake)
end
def to_string(s), do: s
def from_string(int) when is_integer(int) do
from_string(Kernel.to_string(int))
end
for i <- [-1, 0] do
def from_string(unquote(i)), do: <<0::integer-size(128)>>
def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
end
def from_string(<<_::integer-size(128)>> = flake), do: flake
def from_string(string) when is_binary(string) and byte_size(string) < 18 do
case Integer.parse(string) do
{id, ""} -> <<0::integer-size(64), id::integer-size(64)>>
_ -> nil
end
end
def from_string(string) do
string |> decode_base62 |> from_integer
end
def to_integer(<<integer::integer-size(128)>>), do: integer
def from_integer(integer) do
<<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
<<integer::integer-size(128)>>
end
@doc "Generates a Flake"
@spec get :: binary
def get, do: to_string(:gen_server.call(:flake, :get))
# checks that ID is is valid FlakeID
#
@spec is_flake_id?(String.t()) :: boolean
def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true)
defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true)
defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true)
defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true)
defp is_flake_id?([], true), do: true
defp is_flake_id?(_, _), do: false
# -- Ecto.Type API
@impl Ecto.Type
def type, do: :uuid
@impl Ecto.Type
def cast(value) do
{:ok, FlakeId.to_string(value)}
end
@impl Ecto.Type
def load(value) do
{:ok, FlakeId.to_string(value)}
end
@impl Ecto.Type
def dump(value) do
{:ok, FlakeId.from_string(value)}
end
def autogenerate, do: get()
# -- GenServer API
def start_link(_) do
:gen_server.start_link({:local, :flake}, __MODULE__, [], [])
end
@impl GenServer
def init([]) do
{:ok, %FlakeId{node: worker_id(), time: time()}}
end
@impl GenServer
def handle_call(:get, _from, state) do
{flake, new_state} = get(time(), state)
{:reply, flake, new_state}
end
# Matches when the calling time is the same as the state time. Incr. sq
defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
new_state = %FlakeId{time: time, node: node, sq: seq + 1}
{gen_flake(new_state), new_state}
end
# Matches when the times are different, reset sq
defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
new_state = %FlakeId{time: newtime, node: node, sq: 0}
{gen_flake(new_state), new_state}
end
# Error when clock is running backwards
defp get(newtime, %FlakeId{time: time}) when newtime < time do
{:error, :clock_running_backwards}
end
defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
<<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
end
defp nthchar_base62(n) when n <= 9, do: ?0 + n
defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
defp nthchar_base62(n), do: ?a + n - 36
defp encode_base62(<<integer::integer-size(128)>>) do
integer
|> encode_base62([])
|> List.to_string()
end
defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
defp encode_base62(int, []) when int == 0, do: '0'
defp encode_base62(int, acc) when int == 0, do: acc
defp encode_base62(int, acc) do
r = rem(int, 62)
id = div(int, 62)
acc = [nthchar_base62(r) | acc]
encode_base62(id, acc)
end
defp decode_base62(s) do
decode_base62(String.to_charlist(s), 0)
end
defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
do: decode_base62(cs, 62 * acc + (c - ?0))
defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
do: decode_base62(cs, 62 * acc + (c - ?A + 10))
defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
do: decode_base62(cs, 62 * acc + (c - ?a + 36))
defp decode_base62([], acc), do: acc
defp time do
{mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
end
defp worker_id do
<<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
worker
end
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.List do
alias Pleroma.User alias Pleroma.User
schema "lists" do schema "lists" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:title, :string) field(:title, :string)
field(:following, {:array, :string}, default: []) field(:following, {:array, :string}, default: [])
field(:ap_id, :string) field(:ap_id, :string)

View file

@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do
timestamps() timestamps()
end end
def get_all(page, page_size) do def get_all(params) do
from(q in __MODULE__, base_query =
order_by: [desc: q.inserted_at], get_all_query()
|> maybe_filter_by_date(params)
|> maybe_filter_by_user(params)
|> maybe_filter_by_search(params)
query_with_pagination = base_query |> paginate_query(params)
%{
items: Repo.all(query_with_pagination),
count: Repo.aggregate(base_query, :count, :id)
}
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date)
)
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
from(q in query,
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date),
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_user(query, %{user_id: nil}), do: query
defp maybe_filter_by_user(query, %{user_id: user_id}) do
from(q in query,
where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
)
end
defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
do: query
defp maybe_filter_by_search(query, %{search: search}) do
from(q in query,
where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
)
end
defp paginate_query(query, %{page: page, page_size: page_size}) do
from(q in query,
limit: ^page_size, limit: ^page_size,
offset: ^((page - 1) * page_size) offset: ^((page - 1) * page_size)
) )
|> Repo.all()
end end
defp get_all_query do
from(q in __MODULE__,
order_by: [desc: q.inserted_at]
)
end
defp parse_datetime(datetime) do
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
parsed_datetime
end
@spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
subject: %User{} = subject, subject: %User{} = subject,
action: action, action: action,
permission: permission permission: permission
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
subject: user_to_map(subject), "subject" => user_to_map(subject),
action: action, "action" => action,
permission: permission "permission" => permission,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: "report_update", action: "report_update",
subject: %Activity{data: %{"type" => "Flag"}} = subject subject: %Activity{data: %{"type" => "Flag"}} = subject
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "report_update", "action" => "report_update",
subject: report_to_map(subject) "subject" => report_to_map(subject),
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: "report_response", action: "report_response",
subject: %Activity{} = subject, subject: %Activity{} = subject,
text: text text: text
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "report_response", "action" => "report_response",
subject: report_to_map(subject), "subject" => report_to_map(subject),
text: text "text" => text,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{
actor: User,
subject: Activity,
action: String.t(),
sensitive: String.t(),
visibility: String.t()
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: "status_update", action: "status_update",
@ -76,41 +158,49 @@ def insert_log(%{
sensitive: sensitive, sensitive: sensitive,
visibility: visibility visibility: visibility
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "status_update", "action" => "status_update",
subject: status_to_map(subject), "subject" => status_to_map(subject),
sensitive: sensitive, "sensitive" => sensitive,
visibility: visibility "visibility" => visibility,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: "status_delete", action: "status_delete",
subject_id: subject_id subject_id: subject_id
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "status_delete", "action" => "status_delete",
subject_id: subject_id "subject_id" => subject_id,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) :: @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any} {:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: action, "action" => action,
subject: user_to_map(subject) "subject" => user_to_map(subject),
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
@ -118,97 +208,128 @@ def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
subjects = Enum.map(subjects, &user_to_map/1) subjects = Enum.map(subjects, &user_to_map/1)
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: action, "action" => action,
subjects: subjects "subjects" => subjects,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
followed: %User{} = followed, followed: %User{} = followed,
follower: %User{} = follower, follower: %User{} = follower,
action: "follow" action: "follow"
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "follow", "action" => "follow",
followed: user_to_map(followed), "followed" => user_to_map(followed),
follower: user_to_map(follower) "follower" => user_to_map(follower),
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
followed: %User{} = followed, followed: %User{} = followed,
follower: %User{} = follower, follower: %User{} = follower,
action: "unfollow" action: "unfollow"
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: "unfollow", "action" => "unfollow",
followed: user_to_map(followed), "followed" => user_to_map(followed),
follower: user_to_map(follower) "follower" => user_to_map(follower),
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{
actor: User,
action: String.t(),
nicknames: [String.t()],
tags: [String.t()]
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
nicknames: nicknames, nicknames: nicknames,
tags: tags, tags: tags,
action: action action: action
}) do }) do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
nicknames: nicknames, "nicknames" => nicknames,
tags: tags, "tags" => tags,
action: action "action" => action,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end end
@spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: action, action: action,
target: target target: target
}) })
when action in ["relay_follow", "relay_unfollow"] do when action in ["relay_follow", "relay_unfollow"] do
Repo.insert(%ModerationLog{ %ModerationLog{
data: %{ data: %{
actor: user_to_map(actor), "actor" => user_to_map(actor),
action: action, "action" => action,
target: target "target" => target,
"message" => ""
} }
}) }
|> insert_log_entry_with_message()
end
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
|> Repo.insert()
end end
defp user_to_map(%User{} = user) do defp user_to_map(%User{} = user) do
user user
|> Map.from_struct() |> Map.from_struct()
|> Map.take([:id, :nickname]) |> Map.take([:id, :nickname])
|> Map.put(:type, "user") |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end end
defp report_to_map(%Activity{} = report) do defp report_to_map(%Activity{} = report) do
%{ %{
type: "report", "type" => "report",
id: report.id, "id" => report.id,
state: report.data["state"] "state" => report.data["state"]
} }
end end
defp status_to_map(%Activity{} = status) do defp status_to_map(%Activity{} = status) do
%{ %{
type: "status", "type" => "status",
id: status.id "id" => status.id
} }
end end

View file

@ -22,8 +22,8 @@ defmodule Pleroma.Notification do
schema "notifications" do schema "notifications" do
field(:seen, :boolean, default: false) field(:seen, :boolean, default: false)
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: Pleroma.FlakeId) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps() timestamps()
end end

View file

@ -64,6 +64,7 @@ def paginate(query, options, :keyset) do
def paginate(query, options, :offset) do def paginate(query, options, :offset) do
query query
|> restrict(:order, options)
|> restrict(:offset, options) |> restrict(:offset, options)
|> restrict(:limit, options) |> restrict(:limit, options)
end end

View file

@ -12,7 +12,7 @@ defmodule Pleroma.PasswordResetToken do
alias Pleroma.User alias Pleroma.User
schema "password_reset_tokens" do schema "password_reset_tokens" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:token, :string) field(:token, :string)
field(:used, :boolean, default: false) field(:used, :boolean, default: false)

View file

@ -11,10 +11,10 @@ defmodule Pleroma.Registration do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@primary_key {:id, Pleroma.FlakeId, autogenerate: true} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "registrations" do schema "registrations" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:provider, :string) field(:provider, :string)
field(:uid, :string) field(:uid, :string)
field(:info, :map, default: %{}) field(:info, :map, default: %{})

View file

@ -17,7 +17,7 @@ defmodule Pleroma.ScheduledActivity do
@min_offset :timer.minutes(5) @min_offset :timer.minutes(5)
schema "scheduled_activities" do schema "scheduled_activities" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime) field(:scheduled_at, :naive_datetime)
field(:params, :map) field(:params, :map)

View file

@ -12,7 +12,7 @@ defmodule Pleroma.ThreadMute do
require Ecto.Query require Ecto.Query
schema "thread_mutes" do schema "thread_mutes" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:context, :string) field(:context, :string)
end end
@ -24,7 +24,7 @@ def changeset(mute, params \\ %{}) do
end end
def query(user_id, context) do def query(user_id, context) do
user_id = Pleroma.FlakeId.from_string(user_id) {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
ThreadMute ThreadMute
|> Ecto.Query.where(user_id: ^user_id) |> Ecto.Query.where(user_id: ^user_id)

View file

@ -34,7 +34,7 @@ defmodule Pleroma.User do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, Pleroma.FlakeId, autogenerate: true} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@ -106,9 +106,7 @@ def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil def profile_url(_), do: nil
def ap_id(%User{nickname: nickname}) do def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
"#{Web.base_url()}/users/#{nickname}"
end
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@ -119,12 +117,9 @@ def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def user_info(%User{} = user, args \\ %{}) do def user_info(%User{} = user, args \\ %{}) do
following_count = following_count =
if args[:following_count], Map.get(args, :following_count, user.info.following_count || following_count(user))
do: args[:following_count],
else: user.info.following_count || following_count(user)
follower_count = follower_count = Map.get(args, :follower_count, user.info.follower_count)
if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
%{ %{
note_count: user.info.note_count, note_count: user.info.note_count,
@ -137,12 +132,11 @@ def user_info(%User{} = user, args \\ %{}) do
end end
def follow_state(%User{} = user, %User{} = target) do def follow_state(%User{} = user, %User{} = target) do
follow_activity = Utils.fetch_latest_follow(user, target) case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
if follow_activity,
do: follow_activity.data["state"],
# Ideally this would be nil, but then Cachex does not commit the value # Ideally this would be nil, but then Cachex does not commit the value
else: false _ -> false
end
end end
def get_cached_follow_state(user, target) do def get_cached_follow_state(user, target) do
@ -152,11 +146,7 @@ def get_cached_follow_state(user, target) do
@spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()} @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put( Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
:user_cache,
"follow_state:#{user_ap_id}|#{target_ap_id}",
state
)
end end
def set_info_cache(user, args) do def set_info_cache(user, args) do
@ -197,34 +187,25 @@ def remote_user_creation(params) do
|> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit) |> truncate_if_exists(:bio, bio_limit)
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info]) changeset =
%User{local: false}
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar]) |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id]) |> validate_required([:name, :ap_id])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex) |> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit) |> validate_length(:name, max: name_limit)
|> put_change(:local, false) |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
|> put_embed(:info, info_cng)
if changes.valid? do case params[:info][:source_data] do
case info_cng.changes[:source_data] do
%{"followers" => followers, "following" => following} -> %{"followers" => followers, "following" => following} ->
changes changeset
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
|> put_change(:following_address, following) |> put_change(:following_address, following)
_ -> _ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :follower_address, followers)
changes
|> put_change(:follower_address, followers)
end
else
changes
end end
end end
@ -245,7 +226,6 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
struct struct
|> cast(params, [ |> cast(params, [
@ -260,7 +240,7 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit) |> validate_length(:name, max: name_limit)
|> put_embed(:info, info_cng) |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
end end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
@ -311,10 +291,6 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
opts[:need_confirmation] opts[:need_confirmation]
end end
info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset =
struct struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_required([:name, :nickname, :password, :password_confirmation])
@ -326,28 +302,28 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> put_change(:info, info_change) |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
|> maybe_validate_required_email(opts[:external])
changeset = |> put_password_hash
if opts[:external] do |> put_ap_id()
changeset |> unique_constraint(:ap_id)
else |> put_following_and_follower_address()
validate_required(changeset, [:email])
end end
if changeset.valid? do def maybe_validate_required_email(changeset, true), do: changeset
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset changeset
|> put_password_hash
|> put_change(:ap_id, ap_id)
|> unique_constraint(:ap_id)
|> put_change(:following, [followers]) |> put_change(:following, [followers])
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
else
changeset
end
end end
defp autofollow_users(user) do defp autofollow_users(user) do
@ -362,9 +338,8 @@ defp autofollow_users(user) do
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset), with {:ok, user} <- Repo.insert(changeset) do
{:ok, user} <- post_register_action(user) do post_register_action(user)
{:ok, user}
end end
end end
@ -410,7 +385,7 @@ def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
end end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not User.ap_enabled?(followed) do if not ap_enabled?(followed) do
follow(follower, followed) follow(follower, followed)
else else
{:ok, follower} {:ok, follower}
@ -443,9 +418,7 @@ def follow_all(follower, followeds) do
{1, [follower]} = Repo.update_all(q, []) {1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, fn followed -> Enum.each(followeds, &update_follower_count/1)
update_follower_count(followed)
end)
set_cache(follower) set_cache(follower)
end end
@ -555,8 +528,6 @@ def set_cache(%User{} = user) do
def update_and_set_cache(changeset) do def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user) set_cache(user)
else
e -> e
end end
end end
@ -593,9 +564,7 @@ def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}" key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn -> Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname) case get_or_fetch_by_nickname(nickname) do
case user_result do
{:ok, user} -> {:commit, user} {:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil} {:error, _error} -> {:ignore, nil}
end end
@ -606,7 +575,7 @@ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content]) restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
cond do cond do
is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) -> is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id) get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false -> restrict_to_local == false ->
@ -635,13 +604,11 @@ def get_by_nickname_or_email(nickname_or_email) do
def get_cached_user_info(user) do def get_cached_user_info(user) do
key = "user_info:#{user.id}" key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end) Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
end end
def fetch_by_nickname(nickname) do def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname) case ActivityPub.make_user_from_nickname(nickname) do
case ap_try do
{:ok, user} -> {:ok, user} {:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname) _ -> OStatus.make_user(nickname)
end end
@ -676,7 +643,8 @@ def get_followers_query(%User{} = user, nil) do
end end
def get_followers_query(user, page) do def get_followers_query(user, page) do
from(u in get_followers_query(user, nil)) user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20) |> User.Query.paginate(page, 20)
end end
@ -685,25 +653,24 @@ def get_followers_query(user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())} @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do def get_followers(user, page \\ nil) do
q = get_followers_query(user, page) user
|> get_followers_query(page)
{:ok, Repo.all(q)} |> Repo.all()
end end
@spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())} @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do def get_external_followers(user, page \\ nil) do
q =
user user
|> get_followers_query(page) |> get_followers_query(page)
|> User.Query.build(%{external: true}) |> User.Query.build(%{external: true})
|> Repo.all()
{:ok, Repo.all(q)}
end end
def get_followers_ids(user, page \\ nil) do def get_followers_ids(user, page \\ nil) do
q = get_followers_query(user, page) user
|> get_followers_query(page)
Repo.all(from(u in q, select: u.id)) |> select([u], u.id)
|> Repo.all()
end end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
@ -712,7 +679,8 @@ def get_friends_query(%User{} = user, nil) do
end end
def get_friends_query(user, page) do def get_friends_query(user, page) do
from(u in get_friends_query(user, nil)) user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20) |> User.Query.paginate(page, 20)
end end
@ -720,28 +688,27 @@ def get_friends_query(user, page) do
def get_friends_query(user), do: get_friends_query(user, nil) def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do def get_friends(user, page \\ nil) do
q = get_friends_query(user, page) user
|> get_friends_query(page)
{:ok, Repo.all(q)} |> Repo.all()
end end
def get_friends_ids(user, page \\ nil) do def get_friends_ids(user, page \\ nil) do
q = get_friends_query(user, page) user
|> get_friends_query(page)
Repo.all(from(u in q, select: u.id)) |> select([u], u.id)
|> Repo.all()
end end
@spec get_follow_requests(User.t()) :: {:ok, [User.t()]} @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
def get_follow_requests(%User{} = user) do def get_follow_requests(%User{} = user) do
users = user
Activity.follow_requests_for_actor(user) |> Activity.follow_requests_for_actor()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id) |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id) |> group_by([a, u], u.id)
|> select([a, u], u) |> select([a, u], u)
|> Repo.all() |> Repo.all()
{:ok, users}
end end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
@ -787,21 +754,15 @@ def decrease_note_count(%User{} = user) do
end end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
note_count_query = note_count =
from( from(
a in Object, a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data), where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id) select: count(a.id)
) )
|> Repo.one()
note_count = Repo.one(note_count_query) update_info(user, &User.Info.set_note_count(&1, note_count))
info_cng = User.Info.set_note_count(user.info, note_count)
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end end
@spec maybe_fetch_follow_information(User.t()) :: User.t() @spec maybe_fetch_follow_information(User.t()) :: User.t()
@ -818,17 +779,7 @@ def maybe_fetch_follow_information(user) do
def fetch_follow_information(user) do def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
info_cng = User.Info.follow_information_update(user.info, info) update_info(user, &User.Info.follow_information_update(&1, info))
changeset =
user
|> change()
|> put_embed(:info, info_cng)
update_and_set_cache(changeset)
else
{:error, _} = e -> e
e -> {:error, e}
end end
end end
@ -902,62 +853,28 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
info = muter.info update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
info_cng =
User.Info.add_to_mutes(info, ap_id)
|> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def unmute(muter, %{ap_id: ap_id}) do def unmute(muter, %{ap_id: ap_id}) do
info = muter.info update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
info_cng =
User.Info.remove_from_mutes(info, ap_id)
|> User.Info.remove_from_muted_notifications(info, ap_id)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def subscribe(subscriber, %{ap_id: ap_id}) do def subscribe(subscriber, %{ap_id: ap_id}) do
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do if blocks?(subscribed, subscriber) and deny_follow_blocked do
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
if blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else else
info_cng = update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
subscribed.info
|> User.Info.add_to_subscribers(subscriber.ap_id)
change(subscribed)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end end
end end
end end
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do with %User{} = user <- get_cached_by_ap_id(ap_id) do
info_cng = update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
user.info
|> User.Info.remove_from_subscribers(unsubscriber.ap_id)
change(user)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end end
end end
@ -986,21 +903,11 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
blocker blocker
end end
if following?(blocked, blocker) do if following?(blocked, blocker), do: unfollow(blocked, blocker)
unfollow(blocked, blocker)
end
{:ok, blocker} = update_follower_count(blocker) {:ok, blocker} = update_follower_count(blocker)
info_cng = update_info(blocker, &User.Info.add_to_block(&1, ap_id))
blocker.info
|> User.Info.add_to_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
# helper to handle the block given only an actor's AP id # helper to handle the block given only an actor's AP id
@ -1009,15 +916,7 @@ def block(blocker, %{ap_id: ap_id}) do
end end
def unblock(blocker, %{ap_id: ap_id}) do def unblock(blocker, %{ap_id: ap_id}) do
info_cng = update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
blocker.info
|> User.Info.remove_from_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def mutes?(nil, _), do: false def mutes?(nil, _), do: false
@ -1074,27 +973,11 @@ def subscribers(user) do
end end
def block_domain(user, domain) do def block_domain(user, domain) do
info_cng = update_info(user, &User.Info.add_to_domain_block(&1, domain))
user.info
|> User.Info.add_to_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def unblock_domain(user, domain) do def unblock_domain(user, domain) do
info_cng = update_info(user, &User.Info.remove_from_domain_block(&1, domain))
user.info
|> User.Info.remove_from_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def deactivate_async(user, status \\ true) do def deactivate_async(user, status \\ true) do
@ -1102,28 +985,16 @@ def deactivate_async(user, status \\ true) do
end end
def deactivate(%User{} = user, status \\ true) do def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status) with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
Enum.each(get_followers(user), &invalidate_cache/1)
with {:ok, friends} <- User.get_friends(user), Enum.each(get_friends(user), &update_follower_count/1)
{:ok, followers} <- User.get_followers(user),
{:ok, user} <-
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache() do
Enum.each(followers, &invalidate_cache(&1))
Enum.each(friends, &update_follower_count(&1))
{:ok, user} {:ok, user}
end end
end end
def update_notification_settings(%User{} = user, settings \\ %{}) do def update_notification_settings(%User{} = user, settings \\ %{}) do
info_changeset = User.Info.update_notification_settings(user.info, settings) update_info(user, &User.Info.update_notification_settings(&1, settings))
change(user)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end end
def delete(%User{} = user) do def delete(%User{} = user) do
@ -1137,18 +1008,18 @@ def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user) {:ok, _user} = ActivityPub.delete(user)
# Remove all relationships # Remove all relationships
{:ok, followers} = User.get_followers(user) user
|> get_followers()
Enum.each(followers, fn follower -> |> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user) ActivityPub.unfollow(follower, user)
User.unfollow(follower, user) unfollow(follower, user)
end) end)
{:ok, friends} = User.get_friends(user) user
|> get_friends()
Enum.each(friends, fn followed -> |> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed) ActivityPub.unfollow(user, followed)
User.unfollow(user, followed) unfollow(user, followed)
end) end)
delete_user_activities(user) delete_user_activities(user)
@ -1160,13 +1031,11 @@ def perform(:delete, %User{} = user) do
def perform(:fetch_initial_posts, %User{} = user) do def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
Enum.each(
# Insert all the posts in reverse order, so they're in the right order on the timeline # Insert all the posts in reverse order, so they're in the right order on the timeline
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), user.info.source_data["outbox"]
&Pleroma.Web.Federator.incoming_ap_doc/1 |> Utils.fetch_ordered_collection(pages)
) |> Enum.reverse()
|> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
{:ok, user}
end end
def perform(:deactivate_async, user, status), do: deactivate(user, status) def perform(:deactivate_async, user, status), do: deactivate(user, status)
@ -1252,16 +1121,12 @@ def follow_import(%User{} = follower, followed_identifiers)
}) })
end end
def delete_user_activities(%User{ap_id: ap_id} = user) do def delete_user_activities(%User{ap_id: ap_id}) do
ap_id ap_id
|> Activity.Queries.by_actor() |> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50) |> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities -> |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
Enum.each(activities, &delete_activity(&1))
end)
|> Stream.run() |> Stream.run()
{:ok, user}
end end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
@ -1271,17 +1136,19 @@ defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
end end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unlike(user, object) activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unannounce(user, object) activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end end
defp delete_activity(_activity), do: "Doing nothing" defp delete_activity(_activity), do: "Doing nothing"
@ -1293,9 +1160,7 @@ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id) do def fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id) case ActivityPub.make_user_from_ap_id(ap_id) do
case ap_try do
{:ok, user} -> {:ok, user} ->
{:ok, user} {:ok, user}
@ -1310,7 +1175,7 @@ def fetch_by_ap_id(ap_id) do
def get_or_fetch_by_ap_id(ap_id) do def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id) user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do if !is_nil(user) and !needs_update?(user) do
{:ok, user} {:ok, user}
else else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled) # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
@ -1330,18 +1195,19 @@ def get_or_fetch_by_ap_id(ap_id) do
@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing." @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
if user = get_cached_by_ap_id(uri) do with %User{} = user <- get_cached_by_ap_id(uri) do
user user
else else
changes = _ ->
{:ok, user} =
%User{info: %User.Info{}} %User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local]) |> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, uri) |> put_change(:ap_id, uri)
|> put_change(:nickname, nickname) |> put_change(:nickname, nickname)
|> put_change(:local, true) |> put_change(:local, true)
|> put_change(:follower_address, uri <> "/followers") |> put_change(:follower_address, uri <> "/followers")
|> Repo.insert()
{:ok, user} = Repo.insert(changes)
user user
end end
end end
@ -1399,23 +1265,21 @@ def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# this is because we have synchronous follow APIs and need to simulate them # this is because we have synchronous follow APIs and need to simulate them
# with an async handshake # with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- User.get_cached_by_id(a.id), with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do %User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b} {:ok, a, b}
else else
_e -> nil -> :error
:error
end end
end end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout), with :ok <- :timer.sleep(timeout),
%User{} = a <- User.get_cached_by_id(a.id), %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do %User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b} {:ok, a, b}
else else
_e -> nil -> :error
:error
end end
end end
@ -1477,7 +1341,7 @@ defp update_tags(%User{} = user, new_tags) do
defp normalize_tags(tags) do defp normalize_tags(tags) do
[tags] [tags]
|> List.flatten() |> List.flatten()
|> Enum.map(&String.downcase(&1)) |> Enum.map(&String.downcase/1)
end end
defp local_nickname_regex do defp local_nickname_regex do
@ -1570,11 +1434,7 @@ def list_inactive_users_query(inactivity_threshold \\ 7) do
@spec switch_email_notifications(t(), String.t(), boolean()) :: @spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()} {:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do def switch_email_notifications(user, type, status) do
info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status}) update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
change(user)
|> put_embed(:info, info)
|> update_and_set_cache()
end end
@doc """ @doc """
@ -1596,13 +1456,8 @@ def touch_last_digest_emailed_at(user) do
def toggle_confirmation(%User{} = user) do def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending need_confirmation? = !user.info.confirmation_pending
info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
user user
|> change() |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end end
def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
@ -1625,16 +1480,11 @@ def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
} }
end end
def ensure_keys_present(%User{info: info} = user) do def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
user def ensure_keys_present(%User{} = user) do
|> Ecto.Changeset.change() with {:ok, pem} <- Keys.generate_rsa_pem() do
|> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem)) update_info(user, &User.Info.set_keys(&1, pem))
|> update_and_set_cache()
end end
end end
@ -1680,4 +1530,26 @@ def change_email(user, email) do
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> update_and_set_cache() |> update_and_set_cache()
end end
@doc """
Changes `user.info` and returns the user changeset.
`fun` is called with the `user.info`.
"""
def change_info(user, fun) do
changeset = change(user)
info = get_field(changeset, :info) || %User.Info{}
put_embed(changeset, :info, fun.(info))
end
@doc """
Updates `user.info` and sets cache.
`fun` is called with the `user.info`.
"""
def update_info(user, fun) do
user
|> change_info(fun)
|> update_and_set_cache()
end
end end

View file

@ -54,6 +54,7 @@ defmodule Pleroma.User.Info do
field(:pleroma_settings_store, :map, default: %{}) field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: nil) field(:fields, {:array, :map}, default: nil)
field(:raw_fields, {:array, :map}, default: []) field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)
field(:notification_settings, :map, field(:notification_settings, :map,
default: %{ default: %{
@ -187,16 +188,11 @@ def set_subscribers(info, subscribers) do
|> validate_required([:subscribers]) |> validate_required([:subscribers])
end end
@spec add_to_mutes(Info.t(), String.t()) :: Changeset.t() @spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
def add_to_mutes(info, muted) do def add_to_mutes(info, muted, notifications?) do
set_mutes(info, Enum.uniq([muted | info.mutes])) info
end |> set_mutes(Enum.uniq([muted | info.mutes]))
|> set_notification_mutes(
@spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
Changeset.t()
def add_to_muted_notifications(changeset, info, muted, notifications?) do
set_notification_mutes(
changeset,
Enum.uniq([muted | info.muted_notifications]), Enum.uniq([muted | info.muted_notifications]),
notifications? notifications?
) )
@ -204,12 +200,9 @@ def add_to_muted_notifications(changeset, info, muted, notifications?) do
@spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t() @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
def remove_from_mutes(info, muted) do def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted)) info
end |> set_mutes(List.delete(info.mutes, muted))
|> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
@spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
def remove_from_muted_notifications(changeset, info, muted) do
set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
end end
def add_to_block(info, blocked) do def add_to_block(info, blocked) do
@ -277,7 +270,8 @@ def remote_user_creation(info, params) do
:hide_follows_count, :hide_follows_count,
:follower_count, :follower_count,
:fields, :fields,
:following_count :following_count,
:discoverable
]) ])
|> validate_fields(true) |> validate_fields(true)
end end
@ -295,6 +289,7 @@ def user_upgrade(info, params, remote? \\ false) do
:hide_follows, :hide_follows,
:fields, :fields,
:hide_followers, :hide_followers,
:discoverable,
:hide_followers_count, :hide_followers_count,
:hide_follows_count :hide_follows_count
]) ])
@ -318,7 +313,8 @@ def profile_update(info, params) do
:skip_thread_containment, :skip_thread_containment,
:fields, :fields,
:raw_fields, :raw_fields,
:pleroma_settings_store :pleroma_settings_store,
:discoverable
]) ])
|> validate_fields() |> validate_fields()
end end
@ -342,9 +338,7 @@ defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
is_binary(value) &&
String.length(name) <= name_limit &&
String.length(value) <= value_limit String.length(value) <= value_limit
end end

View file

@ -510,7 +510,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do
end end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) :: @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
@ -519,13 +519,13 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|> Repo.one() |> Repo.one()
end end
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"]) opts = Map.drop(opts, ["user"])
[Pleroma.Constants.as_public()] [Pleroma.Constants.as_public()]
|> fetch_activities_query(opts) |> fetch_activities_query(opts)
|> restrict_unlisted() |> restrict_unlisted()
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse() |> Enum.reverse()
end end
@ -834,7 +834,7 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
defp restrict_muted_reblogs(query, _), do: query defp restrict_muted_reblogs(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
defp exclude_poll_votes(query, _) do defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do if has_named_binding?(query, :object) do
@ -918,11 +918,11 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts["user"]) list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts) fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse() |> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"]) |> maybe_update_cc(list_memberships, opts["user"])
end end
@ -953,10 +953,15 @@ def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
) )
end end
def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do def fetch_activities_bounded(
recipients,
recipients_with_public,
opts \\ %{},
pagination \\ :keyset
) do
fetch_activities_query([], opts) fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public) |> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse() |> Enum.reverse()
end end
@ -996,6 +1001,7 @@ defp object_to_user_data(data) do
locked = data["manuallyApprovesFollowers"] || false locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
user_data = %{ user_data = %{
ap_id: data["id"], ap_id: data["id"],
@ -1004,7 +1010,8 @@ defp object_to_user_data(data) do
source_data: data, source_data: data,
banner: banner, banner: banner,
fields: fields, fields: fields,
locked: locked locked: locked,
discoverable: discoverable
}, },
avatar: avatar, avatar: avatar,
name: data["name"], name: data["name"],

View file

@ -231,13 +231,43 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
end end
end end
def outbox(conn, %{"nickname" => nickname} = params) do def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
activities =
if params["max_id"] do
ActivityPub.fetch_user_activities(user, nil, %{
"max_id" => params["max_id"],
# This is a hack because postgres generates inefficient queries when filtering by
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
"include_poll_votes" => true,
"limit" => 10
})
else
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => 10,
"include_poll_votes" => true
})
end
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/outbox"
})
end
end
def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
|> render("outbox.json", %{user: user, max_id: params["max_id"]}) |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end end
end end
@ -315,12 +345,37 @@ def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox( def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params %{"nickname" => nickname, "page" => page?} = params
) do )
when page? in [true, "true"] do
activities =
if params["max_id"] do
ActivityPub.fetch_activities([user.ap_id | user.following], %{
"max_id" => params["max_id"],
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
end
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
|> render("inbox.json", user: user, max_id: params["max_id"]) |> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/inbox"
})
end
def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
end end
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do

View file

@ -111,11 +111,11 @@ defp should_federate?(inbox, public) do
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | [] @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
defp recipients(actor, activity) do defp recipients(actor, activity) do
{:ok, followers} = followers =
if actor.follower_address in activity.recipients do if actor.follower_address in activity.recipients do
User.get_external_followers(actor) User.get_external_followers(actor)
else else
{:ok, []} []
end end
fetchers = fetchers =

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
@ -107,7 +106,8 @@ def render("user.json", %{user: user}) do
}, },
"endpoints" => endpoints, "endpoints" => endpoints,
"attachment" => fields, "attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ emoji_tags "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags,
"discoverable" => user.info.discoverable
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
@ -210,20 +210,16 @@ def render("followers.json", %{user: user} = opts) do
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
def render("outbox.json", %{user: user, max_id: max_qid}) do def render("activity_collection.json", %{iri: iri}) do
params = %{ %{
"limit" => "10" "id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
} }
|> Map.merge(Utils.make_json_ld_header())
params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end end
activities = ActivityPub.fetch_user_activities(user, nil, params) def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
# this is sorted chronologically, so first activity is the newest (max) # this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} = {max_id, min_id, collection} =
if length(activities) > 0 do if length(activities) > 0 do
@ -243,71 +239,14 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
} }
end end
iri = "#{user.ap_id}/outbox" %{
"id" => "#{iri}?max_id=#{max_id}&page=true",
page = %{
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage", "type" => "OrderedCollectionPage",
"partOf" => iri, "partOf" => iri,
"orderedItems" => collection, "orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}" "next" => "#{iri}?max_id=#{min_id}&page=true"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => page
} }
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
end
def render("inbox.json", %{user: user, max_id: max_qid}) do
params = %{
"limit" => "10"
}
params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id
collection =
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
iri = "#{user.ap_id}/inbox"
page = %{
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
end end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do def collection(collection, iri, page, show_items \\ true, total \\ nil) do

View file

@ -18,7 +18,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -254,18 +256,12 @@ def right_add(%{assigns: %{user: admin}} = conn, %{
"nickname" => nickname "nickname" => nickname
}) })
when permission_group in ["moderator", "admin"] do when permission_group in ["moderator", "admin"] do
user = User.get_cached_by_nickname(nickname) info = Map.put(%{}, "is_" <> permission_group, true)
info = {:ok, user} =
%{} nickname
|> Map.put("is_" <> permission_group, true) |> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))
info_cng = User.Info.admin_api_update(user.info, info)
cng =
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_cng)
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "grant", action: "grant",
@ -274,8 +270,6 @@ def right_add(%{assigns: %{user: admin}} = conn, %{
permission: permission_group permission: permission_group
}) })
{:ok, _user} = User.update_and_set_cache(cng)
json(conn, info) json(conn, info)
end end
@ -293,30 +287,24 @@ def right_get(conn, %{"nickname" => nickname}) do
}) })
end end
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
def right_delete( def right_delete(
%{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn, %{assigns: %{user: admin}} = conn,
%{ %{
"permission_group" => permission_group, "permission_group" => permission_group,
"nickname" => nickname "nickname" => nickname
} }
) )
when permission_group in ["moderator", "admin"] do when permission_group in ["moderator", "admin"] do
if admin_nickname == nickname do info = Map.put(%{}, "is_" <> permission_group, false)
render_error(conn, :forbidden, "You can't revoke your own admin status.")
else
user = User.get_cached_by_nickname(nickname)
info = {:ok, user} =
%{} nickname
|> Map.put("is_" <> permission_group, false) |> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))
info_cng = User.Info.admin_api_update(user.info, info)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
{:ok, _user} = User.update_and_set_cache(cng)
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "revoke", action: "revoke",
@ -327,7 +315,6 @@ def right_delete(
json(conn, info) json(conn, info)
end end
end
def right_delete(conn, _) do def right_delete(conn, _) do
render_error(conn, :not_found, "No such permission_group") render_error(conn, :not_found, "No such permission_group")
@ -450,7 +437,10 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
{:ok, token} = Pleroma.PasswordResetToken.create_token(user) {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn conn
|> json(token.token) |> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end end
@doc "Force password reset for a given user" @doc "Force password reset for a given user"
@ -463,13 +453,17 @@ def force_password_reset(conn, %{"nickname" => nickname}) do
end end
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params)
params = params =
params params
|> Map.put("type", "Flag") |> Map.put("type", "Flag")
|> Map.put("skip_preload", true) |> Map.put("skip_preload", true)
|> Map.put("total", true) |> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
reports = ActivityPub.fetch_activities([], params) reports = ActivityPub.fetch_activities([], params, :offset)
conn conn
|> put_view(ReportView) |> put_view(ReportView)
@ -562,7 +556,15 @@ def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def list_log(conn, params) do def list_log(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
log = ModerationLog.get_all(page, page_size) log =
ModerationLog.get_all(%{
page: page,
page_size: page_size,
start_date: params["start_date"],
end_date: params["end_date"],
user_id: params["user_id"],
search: params["search"]
})
conn conn
|> put_view(ModerationLogView) |> put_view(ModerationLogView)

View file

@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
def render("index.json", %{log: log}) do def render("index.json", %{log: log}) do
render_many(log, __MODULE__, "show.json", as: :log_entry) %{
items: render_many(log.items, __MODULE__, "show.json", as: :log_entry),
total: log.count
}
end end
def render("show.json", %{log_entry: log_entry}) do def render("show.json", %{log_entry: log_entry}) do

View file

@ -306,16 +306,14 @@ defp put_emoji(map, text, emojis) do
# Updates the emojis for a user based on their profile # Updates the emojis for a user based on their profile
def update(user) do def update(user) do
emoji = emoji_from_profile(user)
source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
user = user =
with emoji <- emoji_from_profile(user), with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
info_cng <- User.Info.set_source_data(user.info, source_data),
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do
user user
else else
_e -> _e -> user
user
end end
ActivityPub.update(%{ ActivityPub.update(%{
@ -340,34 +338,21 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
} }
} = activity <- get_by_id_or_ap_id(id_or_ap_id), } = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
%{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity), {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
{:ok, activity} {:ok, activity}
else else
%{errors: [pinned_activities: {err, _}]} -> {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
{:error, err} _ -> {:error, dgettext("errors", "Could not pin")}
_ ->
{:error, dgettext("errors", "Could not pin")}
end end
end end
def unpin(id_or_ap_id, user) do def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
%{valid?: true} = info_changeset <- {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
User.Info.remove_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
{:ok, activity} {:ok, activity}
else else
%{errors: [pinned_activities: {err, _}]} -> %{errors: [pinned_activities: {err, _}]} -> {:error, err}
{:error, err} _ -> {:error, dgettext("errors", "Could not unpin")}
_ ->
{:error, dgettext("errors", "Could not unpin")}
end end
end end
@ -462,23 +447,15 @@ defp set_visibility(activity, %{"visibility" => visibility}) do
defp set_visibility(activity, _), do: {:ok, activity} defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, muted) do def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
ap_id = muted.ap_id
if ap_id not in user.info.muted_reblogs do if ap_id not in user.info.muted_reblogs do
info_changeset = User.Info.add_reblog_mute(user.info, ap_id) User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end end
end end
def show_reblogs(user, muted) do def show_reblogs(user, %{ap_id: ap_id} = _muted) do
ap_id = muted.ap_id
if ap_id in user.info.muted_reblogs do if ap_id in user.info.muted_reblogs do
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id) User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end end
end end
end end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
# This is a hack for twidere. # This is a hack for twidere.
def get_by_id_or_ap_id(id) do def get_by_id_or_ap_id(id) do
activity = activity =
with true <- Pleroma.FlakeId.is_flake_id?(id), with true <- FlakeId.flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do %Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity activity
else else

View file

@ -16,7 +16,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.Filter alias Pleroma.Filter
alias Pleroma.HTTP alias Pleroma.HTTP
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -35,7 +34,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@ -153,7 +151,8 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
:hide_follows, :hide_follows,
:hide_favorites, :hide_favorites,
:show_role, :show_role,
:skip_thread_containment :skip_thread_containment,
:discoverable
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value -> add_if_present(acc, params, to_string(key), key, fn value ->
@ -188,14 +187,13 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
end) end)
|> Map.put(:emoji, user_info_emojis) |> Map.put(:emoji, user_info_emojis)
info_cng = User.Info.profile_update(user.info, info_params) changeset =
user
|> User.update_changeset(user_params)
|> User.change_info(&User.Info.profile_update(&1, info_params))
with changeset <- User.update_changeset(user, user_params), with {:ok, user} <- User.update_and_set_cache(changeset) do
changeset <- Changeset.put_embed(changeset, :info, info_cng), if original_user != user, do: CommonAPI.update(user)
{:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
end
json( json(
conn, conn,
@ -225,12 +223,10 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do
end end
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
with new_info <- %{"banner" => %{}}, new_info = %{"banner" => %{}}
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
CommonAPI.update(user)
json(conn, %{url: nil}) json(conn, %{url: nil})
end end
end end
@ -238,9 +234,7 @@ def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
def update_banner(%{assigns: %{user: user}} = conn, params) do def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data}, new_info <- %{"banner" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info), {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
@ -249,10 +243,9 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
end end
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
with new_info <- %{"background" => %{}}, new_info = %{"background" => %{}}
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng), with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
{:ok, _user} <- User.update_and_set_cache(changeset) do
json(conn, %{url: nil}) json(conn, %{url: nil})
end end
end end
@ -260,9 +253,7 @@ def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
def update_background(%{assigns: %{user: user}} = conn, params) do def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background), with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data}, new_info <- %{"background" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info), {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href}) json(conn, %{url: href})
@ -722,49 +713,6 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
end end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(notifications)
|> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id) id = List.wrap(id)
q = from(u in User, where: u.id in ^id) q = from(u in User, where: u.id in ^id)
@ -816,26 +764,16 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
%{} = attachment_data <- Map.put(object.data, "id", object.id), %{} = attachment_data <- Map.put(object.data, "id", object.id),
%{type: type} = rendered <-
StatusView.render("attachment.json", %{attachment: attachment_data}) do
# Reject if not an image # Reject if not an image
if type == "image" do %{type: "image"} = rendered <-
StatusView.render("attachment.json", %{attachment: attachment_data}) do
# Sure! # Sure!
# Save to the user's info # Save to the user's info
info_changeset = User.Info.mascot_update(user.info, rendered) {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
user_changeset = json(conn, rendered)
user
|> Changeset.change()
|> Changeset.put_embed(:info, info_changeset)
{:ok, _user} = User.update_and_set_cache(user_changeset)
conn
|> json(rendered)
else else
render_error(conn, :unsupported_media_type, "mascots can only be images") %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
end
end end
end end
@ -958,12 +896,12 @@ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
end end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do follow_requests = User.get_follow_requests(followed)
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end end
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_cached_by_id(id), with %User{} = follower <- User.get_cached_by_id(id),
@ -1366,11 +1304,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
end end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
info_cng = User.Info.mastodon_settings_update(user.info, settings) with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
with changeset <- Changeset.change(user),
changeset <- Changeset.put_embed(changeset, :info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
json(conn, %{}) json(conn, %{})
else else
e -> e ->

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Notification
alias Pleroma.Web.MastodonAPI.MastodonAPI
# GET /api/v1/notifications
def index(%{assigns: %{user: user}} = conn, params) do
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(notifications)
|> render("index.json", notifications: notifications, for: user)
end
# GET /api/v1/notifications/:id
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
# POST /api/v1/notifications/clear
def clear(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
# POST /api/v1/notifications/dismiss
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
# DELETE /api/v1/notifications/destroy_multiple
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
end

View file

@ -116,6 +116,8 @@ defp do_render("account.json", %{user: user} = opts) do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for])) bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user}) relationship = render("relationship.json", %{user: opts[:for], target: user})
discoverable = user.info.discoverable
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: username_from_nickname(user.nickname), username: username_from_nickname(user.nickname),
@ -139,7 +141,9 @@ defp do_render("account.json", %{user: user} = opts) do
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")), note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false, sensitive: false,
fields: raw_fields, fields: raw_fields,
pleroma: %{} pleroma: %{
discoverable: discoverable
}
}, },
# Pleroma extension # Pleroma extension

View file

@ -20,7 +20,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec) field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false) field(:used, :boolean, default: false)
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:app, App) belongs_to(:app, App)
timestamps() timestamps()

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:refresh_token, :string) field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec) field(:valid_until, :naive_datetime_usec)
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:app, App) belongs_to(:app, App)
timestamps() timestamps()

View file

@ -3,12 +3,33 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
require Logger require Logger
@emoji_dir_path Path.join( def emoji_dir_path do
Path.join(
Pleroma.Config.get!([:instance, :static_dir]), Pleroma.Config.get!([:instance, :static_dir]),
"emoji" "emoji"
) )
end
@cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) @doc """
Lists packs from the remote instance.
Since JS cannot ask remote instances for their packs due to CPS, it has to
be done by the server
"""
def list_from(conn, %{"instance_address" => address}) do
address = String.trim(address)
if shareable_packs_available(address) do
list_resp =
"#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
json(conn, list_resp)
else
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
@doc """ @doc """
Lists the packs available on the instance as JSON. Lists the packs available on the instance as JSON.
@ -17,7 +38,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
a map of "pack directory name" to pack.json contents. a map of "pack directory name" to pack.json contents.
""" """
def list_packs(conn, _params) do def list_packs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) do # Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
pack_infos = pack_infos =
results results
|> Enum.filter(&has_pack_json?/1) |> Enum.filter(&has_pack_json?/1)
@ -28,24 +52,37 @@ def list_packs(conn, _params) do
|> Enum.into(%{}) |> Enum.into(%{})
json(conn, pack_infos) json(conn, pack_infos)
else
{:create_dir, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
{:ls, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{
error:
"Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
})
end end
end end
defp has_pack_json?(file) do defp has_pack_json?(file) do
dir_path = Path.join(@emoji_dir_path, file) dir_path = Path.join(emoji_dir_path(), file)
# Filter to only use the pack.json packs # Filter to only use the pack.json packs
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json")) File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end end
defp load_pack(pack_name) do defp load_pack(pack_name) do
pack_path = Path.join(@emoji_dir_path, pack_name) pack_path = Path.join(emoji_dir_path(), pack_name)
pack_file = Path.join(pack_path, "pack.json") pack_file = Path.join(pack_path, "pack.json")
{pack_name, Jason.decode!(File.read!(pack_file))} {pack_name, Jason.decode!(File.read!(pack_file))}
end end
defp validate_pack({name, pack}) do defp validate_pack({name, pack}) do
pack_path = Path.join(@emoji_dir_path, name) pack_path = Path.join(emoji_dir_path(), name)
if can_download?(pack, pack_path) do if can_download?(pack, pack_path) do
archive_for_sha = make_archive(name, pack, pack_path) archive_for_sha = make_archive(name, pack, pack_path)
@ -79,7 +116,8 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)]) {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
cache_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files)) cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
Cachex.put!( Cachex.put!(
:emoji_packs_cache, :emoji_packs_cache,
@ -115,7 +153,7 @@ defp make_archive(name, pack, pack_dir) do
to download packs that the instance shares. to download packs that the instance shares.
""" """
def download_shared(conn, %{"name" => name}) do def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
pack_file = Path.join(pack_dir, "pack.json") pack_file = Path.join(pack_dir, "pack.json")
with {_, true} <- {:exists?, File.exists?(pack_file)}, with {_, true} <- {:exists?, File.exists?(pack_file)},
@ -139,19 +177,12 @@ def download_shared(conn, %{"name" => name}) do
end end
end end
@doc """ defp shareable_packs_available(address) do
An admin endpoint to request downloading a pack named `pack_name` from the instance
`instance_address`.
If the requested instance's admin chose to share the pack, it will be downloaded
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
shareable_packs_available =
"#{address}/.well-known/nodeinfo" "#{address}/.well-known/nodeinfo"
|> Tesla.get!() |> Tesla.get!()
|> Map.get(:body) |> Map.get(:body)
|> Jason.decode!() |> Jason.decode!()
|> Map.get("links")
|> List.last() |> List.last()
|> Map.get("href") |> Map.get("href")
# Get the actual nodeinfo address and fetch it # Get the actual nodeinfo address and fetch it
@ -160,8 +191,19 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
|> Jason.decode!() |> Jason.decode!()
|> get_in(["metadata", "features"]) |> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs") |> Enum.member?("shareable_emoji_packs")
end
if shareable_packs_available do @doc """
An admin endpoint to request downloading a pack named `pack_name` from the instance
`instance_address`.
If the requested instance's admin chose to share the pack, it will be downloaded
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
address = String.trim(address)
if shareable_packs_available(address) do
full_pack = full_pack =
"#{address}/api/pleroma/emoji/packs/list" "#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!() |> Tesla.get!()
@ -195,7 +237,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
%{body: emoji_archive} <- Tesla.get!(uri), %{body: emoji_archive} <- Tesla.get!(uri),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
local_name = data["as"] || name local_name = data["as"] || name
pack_dir = Path.join(@emoji_dir_path, local_name) pack_dir = Path.join(emoji_dir_path(), local_name)
File.mkdir_p!(pack_dir) File.mkdir_p!(pack_dir)
files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end) files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
@ -233,7 +275,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
Creates an empty pack named `name` which then can be updated via the admin UI. Creates an empty pack named `name` which then can be updated via the admin UI.
""" """
def create(conn, %{"name" => name}) do def create(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
if not File.exists?(pack_dir) do if not File.exists?(pack_dir) do
File.mkdir_p!(pack_dir) File.mkdir_p!(pack_dir)
@ -257,7 +299,7 @@ def create(conn, %{"name" => name}) do
Deletes the pack `name` and all it's files. Deletes the pack `name` and all it's files.
""" """
def delete(conn, %{"name" => name}) do def delete(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
case File.rm_rf(pack_dir) do case File.rm_rf(pack_dir) do
{:ok, _} -> {:ok, _} ->
@ -276,7 +318,7 @@ def delete(conn, %{"name" => name}) do
`new_data` is the new metadata for the pack, that will replace the old metadata. `new_data` is the new metadata for the pack, that will replace the old metadata.
""" """
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_file_p = Path.join([@emoji_dir_path, name, "pack.json"]) pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -360,7 +402,7 @@ def update_file(
conn, conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) do ) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -408,7 +450,7 @@ def update_file(conn, %{
"action" => "remove", "action" => "remove",
"shortcode" => shortcode "shortcode" => shortcode
}) do }) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -443,7 +485,7 @@ def update_file(
conn, conn,
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
) do ) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -513,11 +555,11 @@ def update_file(conn, %{"action" => action}) do
assumed to be emojis and stored in the new `pack.json` file. assumed to be emojis and stored in the new `pack.json` file.
""" """
def import_from_fs(conn, _params) do def import_from_fs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) do with {:ok, results} <- File.ls(emoji_dir_path()) do
imported_pack_names = imported_pack_names =
results results
|> Enum.filter(fn file -> |> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file) dir_path = Path.join(emoji_dir_path(), file)
# Find the directories that do NOT have pack.json # Find the directories that do NOT have pack.json
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json")) File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end) end)
@ -533,7 +575,7 @@ def import_from_fs(conn, _params) do
end end
defp write_pack_json_contents(dir) do defp write_pack_json_contents(dir) do
dir_path = Path.join(@emoji_dir_path, dir) dir_path = Path.join(emoji_dir_path(), dir)
emoji_txt_path = Path.join(dir_path, "emoji.txt") emoji_txt_path = Path.join(dir_path, "emoji.txt")
files_for_pack = files_for_pack(emoji_txt_path, dir_path) files_for_pack = files_for_pack(emoji_txt_path, dir_path)

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.Push.Subscription do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "push_subscriptions" do schema "push_subscriptions" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:token, Token) belongs_to(:token, Token)
field(:endpoint, :string) field(:endpoint, :string)
field(:key_p256dh, :string) field(:key_p256dh, :string)

View file

@ -222,6 +222,7 @@ defmodule Pleroma.Web.Router do
put("/:name", EmojiAPIController, :create) put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete) delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from) post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from)
end end
scope "/packs" do scope "/packs" do
@ -324,11 +325,11 @@ defmodule Pleroma.Web.Router do
get("/favourites", MastodonAPIController, :favourites) get("/favourites", MastodonAPIController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks) get("/bookmarks", MastodonAPIController, :bookmarks)
post("/notifications/clear", MastodonAPIController, :clear_notifications) get("/notifications", NotificationController, :index)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) get("/notifications/:id", NotificationController, :show)
get("/notifications", MastodonAPIController, :notifications) post("/notifications/clear", NotificationController, :clear)
get("/notifications/:id", MastodonAPIController, :get_notification) post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple) delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.TwitterAPI.Controller do defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Ecto.Changeset
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@ -16,15 +15,12 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
action_fallback(:errors) action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid), new_info = [need_confirmation: false]
true <- user.local,
true <- user.info.confirmation_pending, with %User{info: info} = user <- User.get_cached_by_id(uid),
true <- user.info.confirmation_token == token, true <- user.local and info.confirmation_pending and info.confirmation_token == token,
info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false), {:ok, _} <- User.update_info(user, &User.Info.confirmation_changeset(&1, new_info)) do
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change), redirect(conn, to: "/")
{:ok, _} <- User.update_and_set_cache(changeset) do
conn
|> redirect(to: "/")
end end
end end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
field(:state, :string) field(:state, :string)
field(:subscribers, {:array, :string}, default: []) field(:subscribers, {:array, :string}, default: [])
field(:hub, :string) field(:hub, :string)
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps() timestamps()
end end

View file

@ -158,6 +158,7 @@ defp deps do
{:ex_const, "~> 0.2"}, {:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"}, {:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.11.1", only: :test}, {:excoveralls, "~> 0.11.1", only: :test},
{:flake_id, "~> 0.1.0"},
{:mox, "~> 0.5", only: :test} {:mox, "~> 0.5", only: :test}
] ++ oauth_deps() ] ++ oauth_deps()
end end

View file

@ -1,6 +1,7 @@
%{ %{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"},
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]}, "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]},
"base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm"},
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
@ -17,6 +18,7 @@
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"crontab": {:hex, :crontab, "1.1.7", "b9219f0bdc8678b94143655a8f229716c5810c0636a4489f98c0956137e53985", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.7", "b9219f0bdc8678b94143655a8f229716c5810c0636a4489f98c0956137e53985", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm"},
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
@ -34,12 +36,13 @@
"ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"}, "ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
"ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
"excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]}, "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
@ -84,7 +87,7 @@
"quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},

View file

@ -11,6 +11,7 @@
"@id": "ostatus:conversation", "@id": "ostatus:conversation",
"@type": "@id" "@type": "@id"
}, },
"discoverable": "toot:discoverable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers", "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"ostatus": "http://ostatus.org#", "ostatus": "http://ostatus.org#",
"schema": "http://schema.org", "schema": "http://schema.org",

View file

@ -1,47 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FlakeIdTest do
use Pleroma.DataCase
import Kernel, except: [to_string: 1]
import Pleroma.FlakeId
describe "fake flakes (compatibility with older serial integers)" do
test "from_string/1" do
fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
assert from_string("42") == fake_flake
assert from_string(42) == fake_flake
end
test "zero or -1 is a null flake" do
fake_flake = <<0::integer-size(128)>>
assert from_string("0") == fake_flake
assert from_string("-1") == fake_flake
end
test "to_string/1" do
fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
assert to_string(fake_flake) == "42"
end
end
test "ecto type behaviour" do
flake = <<0, 0, 1, 104, 80, 229, 2, 235, 140, 22, 69, 201, 53, 210, 0, 0>>
flake_s = "9eoozpwTul5mjSEDRI"
assert cast(flake) == {:ok, flake_s}
assert cast(flake_s) == {:ok, flake_s}
assert load(flake) == {:ok, flake_s}
assert load(flake_s) == {:ok, flake_s}
assert dump(flake_s) == {:ok, flake}
assert dump(flake) == {:ok, flake}
end
test "is_flake_id?/1" do
assert is_flake_id?("9eoozpwTul5mjSEDRI")
refute is_flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b")
end
end

View file

@ -30,8 +30,7 @@ test "logging user deletion by moderator", %{moderator: moderator, subject1: sub
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
"@#{moderator.nickname} deleted user @#{subject1.nickname}"
end end
test "logging user creation by moderator", %{ test "logging user creation by moderator", %{
@ -48,7 +47,7 @@ test "logging user creation by moderator", %{
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"
end end
@ -63,7 +62,7 @@ test "logging user follow by admin", %{admin: admin, subject1: subject1, subject
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"
end end
@ -78,7 +77,7 @@ test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subje
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"
end end
@ -100,8 +99,7 @@ test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject
tags = ["foo", "bar"] |> Enum.join(", ") tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end end
test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
@ -122,7 +120,7 @@ test "logging user untagged by admin", %{admin: admin, subject1: subject1, subje
tags = ["foo", "bar"] |> Enum.join(", ") tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}" "@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end end
@ -137,8 +135,7 @@ test "logging user grant by moderator", %{moderator: moderator, subject1: subjec
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"
"@#{moderator.nickname} made @#{subject1.nickname} moderator"
end end
test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do
@ -152,7 +149,7 @@ test "logging user revoke by moderator", %{moderator: moderator, subject1: subje
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"
end end
@ -166,7 +163,7 @@ test "logging relay follow", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} followed relay: https://example.org/relay" "@#{moderator.nickname} followed relay: https://example.org/relay"
end end
@ -180,7 +177,7 @@ test "logging relay unfollow", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay" "@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end end
@ -202,7 +199,7 @@ test "logging report update", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"
end end
@ -224,7 +221,7 @@ test "logging report response", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"
end end
@ -242,7 +239,7 @@ test "logging status sensitivity update", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"
end end
@ -260,7 +257,7 @@ test "logging status visibility update", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"
end end
@ -278,7 +275,7 @@ test "logging status sensitivity & visibility update", %{moderator: moderator} d
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"
end end
@ -294,8 +291,7 @@ test "logging status deletion", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) == assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
"@#{moderator.nickname} deleted status ##{note.id}"
end end
end end
end end

View file

@ -77,12 +77,10 @@ test "following and followers count are updated" do
assert length(following) == 2 assert length(following) == 2
assert info.follower_count == 0 assert info.follower_count == 0
info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
{:ok, user} = {:ok, user} =
user user
|> Ecto.Changeset.change(%{following: following ++ following}) |> Ecto.Changeset.change(%{following: following ++ following})
|> Ecto.Changeset.put_embed(:info, info_cng) |> User.change_info(&Ecto.Changeset.change(&1, %{follower_count: 3}))
|> Repo.update() |> Repo.update()
assert length(user.following) == 4 assert length(user.following) == 4

View file

@ -7,7 +7,16 @@ defmodule Pleroma.InstanceTest do
setup do setup do
File.mkdir_p!(tmp_path()) File.mkdir_p!(tmp_path())
on_exit(fn -> File.rm_rf(tmp_path()) end)
on_exit(fn ->
File.rm_rf(tmp_path())
static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/")
if File.exists?(static_dir) do
File.rm_rf(Path.join(static_dir, "robots.txt"))
end
end)
:ok :ok
end end

View file

@ -74,8 +74,8 @@ test "returns all pending follow requests" do
CommonAPI.follow(follower, unlocked) CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked) CommonAPI.follow(follower, locked)
assert {:ok, []} = User.get_follow_requests(unlocked) assert [] = User.get_follow_requests(unlocked)
assert {:ok, [activity]} = User.get_follow_requests(locked) assert [activity] = User.get_follow_requests(locked)
assert activity assert activity
end end
@ -90,7 +90,7 @@ test "doesn't return already accepted or duplicate follow requests" do
CommonAPI.follow(accepted_follower, locked) CommonAPI.follow(accepted_follower, locked)
User.follow(accepted_follower, locked) User.follow(accepted_follower, locked)
assert {:ok, [activity]} = User.get_follow_requests(locked) assert [activity] = User.get_follow_requests(locked)
assert activity assert activity
end end
@ -99,10 +99,10 @@ test "clears follow requests when requester is blocked" do
follower = insert(:user) follower = insert(:user)
CommonAPI.follow(follower, followed) CommonAPI.follow(follower, followed)
assert {:ok, [_activity]} = User.get_follow_requests(followed) assert [_activity] = User.get_follow_requests(followed)
{:ok, _follower} = User.block(followed, follower) {:ok, _follower} = User.block(followed, follower)
assert {:ok, []} = User.get_follow_requests(followed) assert [] = User.get_follow_requests(followed)
end end
test "follow_all follows mutliple users" do test "follow_all follows mutliple users" do
@ -560,7 +560,7 @@ test "it sets the follower_adress" do
test "it enforces the fqn format for nicknames" do test "it enforces the fqn format for nicknames" do
cs = User.remote_user_creation(%{@valid_remote | nickname: "bla"}) cs = User.remote_user_creation(%{@valid_remote | nickname: "bla"})
assert cs.changes.local == false assert Ecto.Changeset.get_field(cs, :local) == false
assert cs.changes.avatar assert cs.changes.avatar
refute cs.valid? refute cs.valid?
end end
@ -584,7 +584,7 @@ test "gets all followers for a given user" do
{:ok, follower_one} = User.follow(follower_one, user) {:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user) {:ok, follower_two} = User.follow(follower_two, user)
{:ok, res} = User.get_followers(user) res = User.get_followers(user)
assert Enum.member?(res, follower_one) assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two) assert Enum.member?(res, follower_two)
@ -600,7 +600,7 @@ test "gets all friends (followed users) for a given user" do
{:ok, user} = User.follow(user, followed_one) {:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two) {:ok, user} = User.follow(user, followed_two)
{:ok, res} = User.get_friends(user) res = User.get_friends(user)
followed_one = User.get_cached_by_ap_id(followed_one.ap_id) followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id) followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
@ -975,7 +975,7 @@ test "hide a user from followers " do
info = User.get_cached_user_info(user2) info = User.get_cached_user_info(user2)
assert info.follower_count == 0 assert info.follower_count == 0
assert {:ok, []} = User.get_followers(user2) assert [] = User.get_followers(user2)
end end
test "hide a user from friends" do test "hide a user from friends" do
@ -991,7 +991,7 @@ test "hide a user from friends" do
assert info.following_count == 0 assert info.following_count == 0
assert User.following_count(user2) == 0 assert User.following_count(user2) == 0
assert {:ok, []} = User.get_friends(user2) assert [] = User.get_friends(user2)
end end
test "hide a user's statuses from timelines and notifications" do test "hide a user's statuses from timelines and notifications" do
@ -1034,7 +1034,7 @@ test "hide a user's statuses from timelines and notifications" do
test ".delete_user_activities deletes all create activities", %{user: user} do test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
{:ok, _} = User.delete_user_activities(user) User.delete_user_activities(user)
# TODO: Remove favorites, repeats, delete activities. # TODO: Remove favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id) refute Activity.get_by_id(activity.id)
@ -1707,4 +1707,22 @@ test "sets password_reset_pending to true", %{user: user} do
assert password_reset_pending assert password_reset_pending
end end
end end
test "change_info/2" do
user = insert(:user)
assert user.info.hide_follows == false
changeset = User.change_info(user, &User.Info.profile_update(&1, %{hide_follows: true}))
assert changeset.changes.info.changes.hide_follows == true
end
test "update_info/2" do
user = insert(:user)
assert user.info.hide_follows == false
assert {:ok, _} = User.update_info(user, &User.Info.profile_update(&1, %{hide_follows: true}))
assert %{info: %{hide_follows: true}} = Repo.get(User, user.id)
assert {:ok, %{info: %{hide_follows: true}}} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
end
end end

View file

@ -479,7 +479,7 @@ test "it returns a note activity in a collection", %{conn: conn} do
conn conn
|> assign(:user, user) |> assign(:user, user)
|> put_req_header("accept", "application/activity+json") |> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox") |> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"] assert response(conn, 200) =~ note_object.data["content"]
end end
@ -567,7 +567,7 @@ test "it returns a note activity in a collection", %{conn: conn} do
conn = conn =
conn conn
|> put_req_header("accept", "application/activity+json") |> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox") |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"] assert response(conn, 200) =~ note_object.data["content"]
end end
@ -579,7 +579,7 @@ test "it returns an announce activity in a collection", %{conn: conn} do
conn = conn =
conn conn
|> put_req_header("accept", "application/activity+json") |> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox") |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"] assert response(conn, 200) =~ announce_activity.data["object"]
end end

View file

@ -647,6 +647,21 @@ test "retrieves ids up to max_id" do
assert last == last_expected assert last == last_expected
end end
test "paginates via offset/limit" do
_first_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(10)
_later_activities = ActivityBuilder.insert_list(10)
first_expected = List.first(activities)
activities =
ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
first = List.first(activities)
assert length(activities) == 20
assert first == first_expected
end
test "doesn't return reblogs for users for whom reblogs have been muted" do test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity) activity = insert(:note_activity)
user = insert(:user) user = insert(:user)

View file

@ -159,7 +159,7 @@ test "sets correct totalItems when follows are hidden but the follow counter is
end end
end end
test "outbox paginates correctly" do test "activity collection page aginates correctly" do
user = insert(:user) user = insert(:user)
posts = posts =
@ -171,13 +171,21 @@ test "outbox paginates correctly" do
# outbox sorts chronologically, newest first, with ten per page # outbox sorts chronologically, newest first, with ten per page
posts = Enum.reverse(posts) posts = Enum.reverse(posts)
%{"first" => %{"next" => next_url}} = %{"next" => next_url} =
UserView.render("outbox.json", %{user: user, max_id: nil}) UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(posts, 10)
})
next_id = Enum.at(posts, 9).id next_id = Enum.at(posts, 9).id
assert next_url =~ next_id assert next_url =~ next_id
%{"next" => next_url} = UserView.render("outbox.json", %{user: user, max_id: next_id}) %{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(Enum.drop(posts, 10), 10)
})
next_id = Enum.at(posts, 19).id next_id = Enum.at(posts, 19).id
assert next_url =~ next_id assert next_url =~ next_id
end end

View file

@ -586,7 +586,9 @@ test "/api/pleroma/admin/users/:nickname/password_reset" do
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
assert conn.status == 200 resp = json_response(conn, 200)
assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end end
describe "GET /api/pleroma/admin/users" do describe "GET /api/pleroma/admin/users" do
@ -2255,8 +2257,9 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do
describe "GET /api/pleroma/admin/moderation_log" do describe "GET /api/pleroma/admin/moderation_log" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
moderator = insert(:user, info: %{is_moderator: true})
%{conn: assign(conn, :user, admin), admin: admin} %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}
end end
test "returns the log", %{conn: conn, admin: admin} do test "returns the log", %{conn: conn, admin: admin} do
@ -2289,9 +2292,9 @@ test "returns the log", %{conn: conn, admin: admin} do
conn = get(conn, "/api/pleroma/admin/moderation_log") conn = get(conn, "/api/pleroma/admin/moderation_log")
response = json_response(conn, 200) response = json_response(conn, 200)
[first_entry, second_entry] = response [first_entry, second_entry] = response["items"]
assert response |> length() == 2 assert response["total"] == 2
assert first_entry["data"]["action"] == "relay_unfollow" assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] == assert first_entry["message"] ==
@ -2333,9 +2336,10 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
response1 = json_response(conn1, 200) response1 = json_response(conn1, 200)
[first_entry] = response1 [first_entry] = response1["items"]
assert response1 |> length() == 1 assert response1["total"] == 2
assert response1["items"] |> length() == 1
assert first_entry["data"]["action"] == "relay_unfollow" assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] == assert first_entry["message"] ==
@ -2344,14 +2348,119 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
response2 = json_response(conn2, 200) response2 = json_response(conn2, 200)
[second_entry] = response2 [second_entry] = response2["items"]
assert response2 |> length() == 1 assert response2["total"] == 2
assert response2["items"] |> length() == 1
assert second_entry["data"]["action"] == "relay_follow" assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] == assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay" "@#{admin.nickname} followed relay: https://example.org/relay"
end end
test "filters log by date", %{conn: conn, admin: admin} do
first_date = "2017-08-15T15:47:06Z"
second_date = "2017-08-20T15:47:06Z"
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(first_date)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(second_date)
})
conn1 =
get(
conn,
"/api/pleroma/admin/moderation_log?start_date=#{second_date}"
)
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
end
test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
}
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => moderator.id,
"nickname" => moderator.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
}
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id
end
test "returns log filtered by search", %{conn: conn, moderator: moderator} do
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_follow",
target: "https://example.org/relay"
})
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_unfollow",
target: "https://example.org/relay"
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "message"]) ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
end end
describe "PATCH /users/:nickname/force_password_reset" do describe "PATCH /users/:nickname/force_password_reset" do

View file

@ -0,0 +1,299 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "list of notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
"hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
end
test "getting a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
"hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
end
test "dismissing a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200)
end
test "clearing all notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/clear")
assert %{} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/notifications")
assert all = json_response(conn, 200)
assert all == []
end
test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
notification1_id = get_notification_id_by_activity(activity1)
notification2_id = get_notification_id_by_activity(activity2)
notification3_id = get_notification_id_by_activity(activity3)
notification4_id = get_notification_id_by_activity(activity4)
conn = assign(conn, :user, user)
# min_id
result =
conn
|> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
|> json_response(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
# since_id
result =
conn
|> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
|> json_response(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
# max_id
result =
conn
|> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
|> json_response(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
test "filters notifications using exclude_types", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
{:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
mention_notification_id = get_notification_id_by_activity(mention_activity)
favorite_notification_id = get_notification_id_by_activity(favorite_activity)
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
conn = assign(conn, :user, user)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
end
test "destroy multiple", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
{:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
notification1_id = get_notification_id_by_activity(activity1)
notification2_id = get_notification_id_by_activity(activity2)
notification3_id = get_notification_id_by_activity(activity3)
notification4_id = get_notification_id_by_activity(activity4)
conn = assign(conn, :user, user)
result =
conn
|> get("/api/v1/notifications")
|> json_response(:ok)
assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
conn2 =
conn
|> assign(:user, other_user)
result =
conn2
|> get("/api/v1/notifications")
|> json_response(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
conn_destroy =
conn
|> delete("/api/v1/notifications/destroy_multiple", %{
"ids" => [notification1_id, notification2_id]
})
assert json_response(conn_destroy, 200) == %{}
result =
conn2
|> get("/api/v1/notifications")
|> json_response(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
test "doesn't see notifications after muting user with notifications", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert json_response(conn, 200) == []
end
test "see notifications after muting user without notifications", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2, false)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
end
test "see notifications after muting user with notifications and with_muted parameter", %{
conn: conn
} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
assert length(json_response(conn, 200)) == 1
end
defp get_notification_id_by_activity(%{id: id}) do
Notification
|> Repo.get_by(activity_id: id)
|> Map.get(:id)
|> to_string()
end
end

View file

@ -999,299 +999,6 @@ test "list timeline does not leak non-public statuses for unfollowed users", %{c
end end
end end
describe "notifications" do
test "list of notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
~s(hi <span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id
}" rel="ugc">@<span>#{user.nickname}</span></a></span>)
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
end
test "getting a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
~s(hi <span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id
}" rel="ugc">@<span>#{user.nickname}</span></a></span>)
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
end
test "dismissing a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200)
end
test "clearing all notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/clear")
assert %{} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/notifications")
assert all = json_response(conn, 200)
assert all == []
end
test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
conn =
conn
|> assign(:user, user)
# min_id
conn_res =
conn
|> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
# since_id
conn_res =
conn
|> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
# max_id
conn_res =
conn
|> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
test "filters notifications using exclude_types", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
{:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
mention_notification_id =
Repo.get_by(Notification, activity_id: mention_activity.id).id |> to_string()
favorite_notification_id =
Repo.get_by(Notification, activity_id: favorite_activity.id).id |> to_string()
reblog_notification_id =
Repo.get_by(Notification, activity_id: reblog_activity.id).id |> to_string()
follow_notification_id =
Repo.get_by(Notification, activity_id: follow_activity.id).id |> to_string()
conn =
conn
|> assign(:user, user)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
end
test "destroy multiple", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
{:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
conn =
conn
|> assign(:user, user)
conn_res =
conn
|> get("/api/v1/notifications")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
conn2 =
conn
|> assign(:user, other_user)
conn_res =
conn2
|> get("/api/v1/notifications")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
conn_destroy =
conn
|> delete("/api/v1/notifications/destroy_multiple", %{
"ids" => [notification1_id, notification2_id]
})
assert json_response(conn_destroy, 200) == %{}
conn_res =
conn2
|> get("/api/v1/notifications")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
test "doesn't see notifications after muting user with notifications", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert json_response(conn, 200) == []
end
test "see notifications after muting user without notifications", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2, false)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
end
test "see notifications after muting user with notifications and with_muted parameter", %{
conn: conn
} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
conn = assign(conn, :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
assert length(json_response(conn, 200)) == 1
end
end
describe "reblogging" do describe "reblogging" do
test "reblogs and returns the reblogged status", %{conn: conn} do test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
@ -2613,14 +2320,11 @@ test "get instance stats", %{conn: conn} do
{:ok, _} = CommonAPI.post(user, %{"status" => "cofe"}) {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
# Stats should count users with missing or nil `info.deactivated` value # Stats should count users with missing or nil `info.deactivated` value
user = User.get_cached_by_id(user.id)
info_change = Changeset.change(user.info, %{deactivated: nil})
{:ok, _user} = {:ok, _user} =
user user.id
|> Changeset.change() |> User.get_cached_by_id()
|> Changeset.put_embed(:info, info_change) |> User.update_info(&Changeset.change(&1, %{deactivated: nil}))
|> User.update_and_set_cache()
Pleroma.Stats.force_update() Pleroma.Stats.force_update()
@ -3953,13 +3657,9 @@ test "it returns 400 when user is not local", %{conn: conn, user: user} do
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do setup do
user = insert(:user)
info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} = {:ok, user} =
user insert(:user)
|> Changeset.change() |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
|> Changeset.put_embed(:info, info_change)
|> Repo.update() |> Repo.update()
assert user.info.confirmation_pending assert user.info.confirmation_pending

View file

@ -67,7 +67,9 @@ test "Represent a user account" do
source: %{ source: %{
note: "valid html", note: "valid html",
sensitive: false, sensitive: false,
pleroma: %{}, pleroma: %{
discoverable: false
},
fields: [] fields: []
}, },
pleroma: %{ pleroma: %{
@ -137,7 +139,9 @@ test "Represent a Service(bot) account" do
source: %{ source: %{
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{}, pleroma: %{
discoverable: false
},
fields: [] fields: []
}, },
pleroma: %{ pleroma: %{
@ -310,7 +314,9 @@ test "represent an embedded relationship" do
source: %{ source: %{
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{}, pleroma: %{
discoverable: false
},
fields: [] fields: []
}, },
pleroma: %{ pleroma: %{

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@ -775,15 +776,11 @@ test "rejects token exchange with invalid client credentials" do
test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
Pleroma.Config.put([:instance, :account_activation_required], true) Pleroma.Config.put([:instance, :account_activation_required], true)
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} = {:ok, user} =
user insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
|> Ecto.Changeset.change() |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
|> Ecto.Changeset.put_embed(:info, info_change)
|> Repo.update() |> Repo.update()
refute Pleroma.User.auth_active?(user) refute Pleroma.User.auth_active?(user)

View file

@ -50,20 +50,16 @@ test "decodes a salmon with a changed magic key", %{conn: conn} do
assert response(conn, 200) assert response(conn, 200)
end) =~ "[error]" end) =~ "[error]"
# Set a wrong magic-key for a user so it has to refetch
salmon_user = User.get_cached_by_ap_id("http://gs.example.org:4040/index.php/user/1")
# Wrong key # Wrong key
info_cng = info = %{
User.Info.remote_user_creation(salmon_user.info, %{
magic_key: magic_key:
"RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
}) }
salmon_user # Set a wrong magic-key for a user so it has to refetch
|> Ecto.Changeset.change() "http://gs.example.org:4040/index.php/user/1"
|> Ecto.Changeset.put_embed(:info, info_cng) |> User.get_cached_by_ap_id()
|> User.update_and_set_cache() |> User.update_info(&User.Info.remote_user_creation(&1, info))
assert capture_log(fn -> assert capture_log(fn ->
conn = conn =

View file

@ -33,6 +33,28 @@ test "shared & non-shared pack information in list_packs is ok" do
refute pack["pack"]["can-download"] refute pack["pack"]["can-download"]
end end
test "listing remote packs" do
admin = insert(:user, info: %{is_admin: true})
conn = build_conn() |> assign(:user, admin)
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
json(resp)
end)
assert conn
|> post(emoji_api_path(conn, :list_from), %{instance_address: "https://example.com"})
|> json_response(200) == resp
end
test "downloading a shared pack from download_shared" do test "downloading a shared pack from download_shared" do
conn = build_conn() conn = build_conn()
@ -55,13 +77,13 @@ test "downloading shared & unshared packs from another instance via download_fro
mock(fn mock(fn
%{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
json([%{href: "https://old-instance/nodeinfo/2.1.json"}]) json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: []}}) json(%{metadata: %{features: []}})
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json([%{href: "https://example.com/nodeinfo/2.1.json"}]) json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}}) json(%{metadata: %{features: ["shareable_emoji_packs"]}})