Merge remote-tracking branch 'origin/develop' into features/mastoapi/2.6.0-conversations

This commit is contained in:
lain 2019-03-25 13:37:32 +01:00
commit 037fefe218
295 changed files with 1003 additions and 342 deletions

View file

@ -8,8 +8,6 @@
# General application configuration # General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo] config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
config :pleroma, Pleroma.Captcha, config :pleroma, Pleroma.Captcha,
enabled: false, enabled: false,
seconds_valid: 60, seconds_valid: 60,
@ -174,7 +172,8 @@
no_attachment_links: false, no_attachment_links: false,
welcome_user_nickname: nil, welcome_user_nickname: nil,
welcome_message: nil, welcome_message: nil,
max_report_comment_size: 1000 max_report_comment_size: 1000,
safe_dm_mentions: false
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
@ -273,8 +272,6 @@
config :pleroma, :chat, enabled: true config :pleroma, :chat, enabled: true
config :ecto, json_library: Jason
config :phoenix, :format_encoders, json: Jason config :phoenix, :format_encoders, json: Jason
config :pleroma, :gopher, config :pleroma, :gopher,

View file

@ -19,6 +19,7 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance. - `local`: true if the post was made on the local instance.
- `conversation_id`: the ID of the conversation the status is associated with (if any)
## Attachments ## Attachments
@ -29,3 +30,17 @@ Has these additional fields under the `pleroma` object:
## Accounts ## Accounts
- `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc. - `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc.
Has these additional fields under the `pleroma` object:
- `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship
- `is_moderator`: boolean, true if user is a moderator
- `is_admin`: boolean, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
## Notifications
Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user

View file

@ -101,7 +101,8 @@ config :pleroma, Pleroma.Mailer,
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses * `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses
* `welcome_message`: A message that will be send to a newly registered users as a direct message. * `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message. * `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_size`: The maximum size of the report comment (Default: `1000`) * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`)
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
## :logger ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
@ -190,6 +191,7 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `enabled`: Enables the gopher interface * `enabled`: Enables the gopher interface
* `ip`: IP address to bind to * `ip`: IP address to bind to
* `port`: Port to bind to * `port`: Port to bind to
* `dstport`: Port advertised in urls (optional, defaults to `port`)
## :activitypub ## :activitypub
* ``accept_blocks``: Whether to accept incoming block activities from other instances * ``accept_blocks``: Whether to accept incoming block activities from other instances

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Activity do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
import Ecto.Query import Ecto.Query
@ -22,6 +23,10 @@ defmodule Pleroma.Activity do
"Like" => "favourite" "Like" => "favourite"
} }
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -29,9 +34,42 @@ defmodule Pleroma.Activity do
field(:recipients, {:array, :string}) field(:recipients, {:array, :string})
has_many(:notifications, Notification, on_delete: :delete_all) has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
# The foreign key is embedded in a jsonb field.
#
# To use it, you probably want to do an inner join and a preload:
#
# ```
# |> join(:inner, [activity], o in Object,
# on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
# o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object])
# ```
#
# As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
# typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
timestamps() timestamps()
end end
def with_preloaded_object(query) do
query
|> join(
:inner,
[activity],
o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
)
)
|> preload([activity, object], object: object)
end
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.one( Repo.one(
from( from(
@ -41,10 +79,44 @@ def get_by_ap_id(ap_id) do
) )
end end
def get_by_ap_id_with_object(ap_id) do
Repo.one(
from(
activity in Activity,
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)),
left_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
)
end
def get_by_id(id) do def get_by_id(id) do
Repo.get(Activity, id) Repo.get(Activity, id)
end end
def get_by_id_with_object(id) do
from(activity in Activity,
where: activity.id == ^id,
inner_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
|> Repo.one()
end
def by_object_ap_id(ap_id) do def by_object_ap_id(ap_id) do
from( from(
activity in Activity, activity in Activity,
@ -72,7 +144,7 @@ def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do
) )
end end
def create_by_object_ap_id(ap_id) do def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
from( from(
activity in Activity, activity in Activity,
where: where:
@ -86,6 +158,8 @@ def create_by_object_ap_id(ap_id) do
) )
end end
def create_by_object_ap_id(_), do: nil
def get_all_create_by_object_ap_id(ap_id) do def get_all_create_by_object_ap_id(ap_id) do
Repo.all(create_by_object_ap_id(ap_id)) Repo.all(create_by_object_ap_id(ap_id))
end end
@ -97,8 +171,39 @@ def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
def get_create_by_object_ap_id(_), do: nil def get_create_by_object_ap_id(_), do: nil
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"]) def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id) from(
activity in Activity,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^to_string(ap_id)
),
where: fragment("(?)->>'type' = 'Create'", activity.data),
inner_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
end
def create_by_object_ap_id_with_object(_), do: nil
def get_create_by_object_ap_id_with_object(ap_id) do
ap_id
|> create_by_object_ap_id_with_object()
|> Repo.one()
end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil def normalize(_), do: nil
def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
@ -109,7 +214,8 @@ def get_in_reply_to_activity(_), do: nil
def delete_by_ap_id(id) when is_binary(id) do def delete_by_ap_id(id) when is_binary(id) do
by_object_ap_id(id) by_object_ap_id(id)
|> Repo.delete_all(returning: true) |> select([u], u)
|> Repo.delete_all()
|> elem(1) |> elem(1)
|> Enum.find(fn |> Enum.find(fn
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
@ -126,6 +232,10 @@ def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
def mastodon_notification_type(%Activity{}), do: nil def mastodon_notification_type(%Activity{}), do: nil
def from_mastodon_notification_type(type) do
Map.get(@mastodon_to_ap_notification_types, type)
end
def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: [] def all_by_actor_and_id(_actor, []), do: []

View file

@ -29,9 +29,13 @@ def report(to, reporter, account, statuses, comment) do
if length(statuses) > 0 do if length(statuses) > 0 do
statuses_list_html = statuses_list_html =
statuses statuses
|> Enum.map(fn %{id: id} -> |> Enum.map(fn
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id) %{id: id} ->
"<li><a href=\"#{status_url}\">#{status_url}</li>" status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
"<li><a href=\"#{status_url}\">#{status_url}</li>"
id when is_binary(id) ->
"<li><a href=\"#{id}\">#{id}</li>"
end) end)
|> Enum.join("\n") |> Enum.join("\n")

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui @link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
@ -45,15 +46,28 @@ def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
@doc """ @doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags. Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
""" """
@spec linkify(String.t(), keyword()) :: @spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]} {String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do def linkify(text, options \\ []) do
options = options ++ @auto_linker_config options = options ++ @auto_linker_config
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)} if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
end end
def emojify(text) do def emojify(text) do

View file

@ -66,7 +66,8 @@ def info(text) do
def link(name, selector, type \\ 1) do def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host() address = Pleroma.Web.Endpoint.host()
port = Pleroma.Config.get([:gopher, :port], 1234) port = Pleroma.Config.get([:gopher, :port], 1234)
"#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n" dstport = Pleroma.Config.get([:gopher, :dstport], port)
"#{type}#{name}\t#{selector}\t#{address}\t#{dstport}\r\n"
end end
def render_activities(activities) do def render_activities(activities) do

View file

@ -95,6 +95,13 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
# paragraphs and linebreaks # paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", []) Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", []) Meta.allow_tag_with_these_attributes("p", [])
@ -137,6 +144,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("abbr", ["title"]) Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", []) Meta.allow_tag_with_these_attributes("b", [])

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do schema "instances" do
field(:host, :string) field(:host, :string)
field(:unreachable_since, :naive_datetime) field(:unreachable_since, :naive_datetime_usec)
timestamps() timestamps()
end end

View file

@ -7,6 +7,8 @@ defmodule Pleroma.Notification do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -28,36 +30,25 @@ def changeset(%Notification{} = notification, attrs) do
|> cast(attrs, [:seen]) |> cast(attrs, [:seen])
end end
# TODO: Make generic and unify (see activity_pub.ex) def for_user_query(user) do
defp restrict_max(query, %{"max_id" => max_id}) do Notification
from(activity in query, where: activity.id < ^max_id) |> where(user_id: ^user.id)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
end end
defp restrict_max(query, _), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
def for_user(user, opts \\ %{}) do def for_user(user, opts \\ %{}) do
query = user
from( |> for_user_query()
n in Notification, |> Pagination.fetch_paginated(opts)
where: n.user_id == ^user.id,
order_by: [desc: n.id],
join: activity in assoc(n, :activity),
preload: [activity: activity],
limit: 20
)
query =
query
|> restrict_since(opts)
|> restrict_max(opts)
Repo.all(query)
end end
def set_read_up_to(%{id: user_id} = _user, id) do def set_read_up_to(%{id: user_id} = _user, id) do

View file

@ -14,6 +14,8 @@ defmodule Pleroma.Object do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
require Logger
schema "objects" do schema "objects" do
field(:data, :map) field(:data, :map)
@ -38,6 +40,33 @@ def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id))) Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end end
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Activity{object: %Object{} = object}), do: object
# Catch and log Object.normalize() calls where the Activity's child object is not
# preloaded.
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
normalize(ap_id)
end
def normalize(%Activity{data: %{"object" => ap_id}}) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
normalize(ap_id)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}), do: normalize(ap_id) def normalize(%{"id" => ap_id}), do: normalize(ap_id)
def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id) def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(_), do: nil def normalize(_), do: nil

78
lib/pleroma/pagination.ex Normal file
View file

@ -0,0 +1,78 @@
defmodule Pleroma.Pagination do
@moduledoc """
Implements Mastodon-compatible pagination.
"""
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
@default_limit 20
def fetch_paginated(query, params) do
options = cast_params(params)
query
|> paginate(options)
|> Repo.all()
|> enforce_order(options)
end
def paginate(query, options) do
query
|> restrict(:min_id, options)
|> restrict(:since_id, options)
|> restrict(:max_id, options)
|> restrict(:order, options)
|> restrict(:limit, options)
end
defp cast_params(params) do
param_types = %{
min_id: :string,
since_id: :string,
max_id: :string,
limit: :integer
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :min_id, %{min_id: min_id}) do
where(query, [q], q.id > ^min_id)
end
defp restrict(query, :since_id, %{since_id: since_id}) do
where(query, [q], q.id > ^since_id)
end
defp restrict(query, :max_id, %{max_id: max_id}) do
where(query, [q], q.id < ^max_id)
end
defp restrict(query, :order, %{min_id: _}) do
order_by(query, [u], fragment("? asc nulls last", u.id))
end
defp restrict(query, :order, _options) do
order_by(query, [u], fragment("? desc nulls last", u.id))
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)
query
|> limit(^limit)
end
defp restrict(query, _, _), do: query
defp enforce_order(result, %{min_id: _}) do
result
|> Enum.reverse()
end
defp enforce_order(result, _), do: result
end

View file

@ -3,7 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo do defmodule Pleroma.Repo do
use Ecto.Repo, otp_app: :pleroma use Ecto.Repo,
otp_app: :pleroma,
adapter: Ecto.Adapters.Postgres,
migration_timestamps: [type: :naive_datetime_usec]
@doc """ @doc """
Dynamically loads the repository url from the Dynamically loads the repository url from the

View file

@ -13,10 +13,15 @@ def get_file(file) do
bucket = Keyword.fetch!(config, :bucket) bucket = Keyword.fetch!(config, :bucket)
bucket_with_namespace = bucket_with_namespace =
if namespace = Keyword.get(config, :bucket_namespace) do cond do
namespace <> ":" <> bucket truncated_namespace = Keyword.get(config, :truncated_namespace) ->
else truncated_namespace
bucket
namespace = Keyword.get(config, :bucket_namespace) ->
namespace <> ":" <> bucket
true ->
bucket
end end
{:ok, {:ok,

View file

@ -50,9 +50,10 @@ defmodule Pleroma.User do
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:follower_address, :string) field(:follower_address, :string)
field(:search_rank, :float, virtual: true) field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: []) field(:tags, {:array, :string}, default: [])
field(:bookmarks, {:array, :string}, default: []) field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime) field(:last_refreshed_at, :naive_datetime_usec)
has_many(:notifications, Notification) has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info) embeds_one(:info, Pleroma.User.Info)
@ -104,9 +105,8 @@ def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}" "#{Web.base_url()}/users/#{nickname}"
end end
def ap_followers(%User{} = user) do def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
"#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
end
def user_info(%User{} = user) do def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0 oneself = if user.local, do: 1, else: 0
@ -335,10 +335,11 @@ def follow_all(follower, followeds) do
^followed_addresses ^followed_addresses
) )
] ]
] ],
select: u
) )
{1, [follower]} = Repo.update_all(q, [], returning: true) {1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, fn followed -> Enum.each(followeds, fn followed ->
update_follower_count(followed) update_follower_count(followed)
@ -368,10 +369,11 @@ def follow(%User{} = follower, %User{info: info} = followed) do
q = q =
from(u in User, from(u in User,
where: u.id == ^follower.id, where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]] update: [push: [following: ^ap_followers]],
select: u
) )
{1, [follower]} = Repo.update_all(q, [], returning: true) {1, [follower]} = Repo.update_all(q, [])
{:ok, _} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
@ -386,10 +388,11 @@ def unfollow(%User{} = follower, %User{} = followed) do
q = q =
from(u in User, from(u in User,
where: u.id == ^follower.id, where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]] update: [pull: [following: ^ap_followers]],
select: u
) )
{1, [follower]} = Repo.update_all(q, [], returning: true) {1, [follower]} = Repo.update_all(q, [])
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -637,7 +640,7 @@ def get_follow_requests(%User{} = user) do
users = users =
user user
|> User.get_follow_requests_query() |> User.get_follow_requests_query()
|> join(:inner, [a], u in User, a.actor == u.ap_id) |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id) |> group_by([a, u], u.id)
|> select([a, u], u) |> select([a, u], u)
@ -659,7 +662,8 @@ def increase_note_count(%User{} = user) do
) )
] ]
) )
|> Repo.update_all([], returning: true) |> select([u], u)
|> Repo.update_all([])
|> case do |> case do
{1, [user]} -> set_cache(user) {1, [user]} -> set_cache(user)
_ -> {:error, user} _ -> {:error, user}
@ -679,7 +683,8 @@ def decrease_note_count(%User{} = user) do
) )
] ]
) )
|> Repo.update_all([], returning: true) |> select([u], u)
|> Repo.update_all([])
|> case do |> case do
{1, [user]} -> set_cache(user) {1, [user]} -> set_cache(user)
_ -> {:error, user} _ -> {:error, user}
@ -725,7 +730,8 @@ def update_follower_count(%User{} = user) do
) )
] ]
) )
|> Repo.update_all([], returning: true) |> select([u], u)
|> Repo.update_all([])
|> case do |> case do
{1, [user]} -> set_cache(user) {1, [user]} -> set_cache(user)
_ -> {:error, user} _ -> {:error, user}
@ -773,7 +779,7 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
}) :: {:ok, [Pleroma.User.t()], number()} }) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
query = query =
from(u in User, order_by: u.id) from(u in User, order_by: u.nickname)
|> maybe_local_user_query(local) |> maybe_local_user_query(local)
paginated_query = paginated_query =
@ -789,34 +795,27 @@ def search_for_admin(%{query: nil, local: local, page: page, page_size: page_siz
@spec search_for_admin(%{ @spec search_for_admin(%{
query: binary(), query: binary(),
admin: Pleroma.User.t(),
local: boolean(), local: boolean(),
page: number(), page: number(),
page_size: number() page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()} }) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{ def search_for_admin(%{
query: term, query: term,
admin: admin,
local: local, local: local,
page: page, page: page,
page_size: page_size page_size: page_size
}) do }) do
term = String.trim_leading(term, "@") maybe_local_query = User |> maybe_local_user_query(local)
local_paginated_query = search_query = from(u in maybe_local_query, where: ilike(u.nickname, ^"%#{term}%"))
User count = search_query |> Repo.aggregate(:count, :id)
|> maybe_local_user_query(local)
results =
search_query
|> paginate(page, page_size) |> paginate(page, page_size)
|> Repo.all()
search_query = fts_search_subquery(term, local_paginated_query) {:ok, results, count}
count =
term
|> fts_search_subquery()
|> maybe_local_user_query(local)
|> Repo.aggregate(:count, :id)
{:ok, do_search(search_query, admin), count}
end end
def search(query, resolve \\ false, for_user \\ nil) do def search(query, resolve \\ false, for_user \\ nil) do
@ -825,31 +824,53 @@ def search(query, resolve \\ false, for_user \\ nil) do
if resolve, do: get_or_fetch(query) if resolve, do: get_or_fetch(query)
fts_results = do_search(fts_search_subquery(query), for_user) {:ok, results} =
{:ok, trigram_results} =
Repo.transaction(fn -> Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
do_search(trigram_search_subquery(query), for_user) Repo.all(search_query(query, for_user))
end) end)
Enum.uniq_by(fts_results ++ trigram_results, & &1.id) results
end end
defp do_search(subquery, for_user, options \\ []) do def search_query(query, for_user) do
q = fts_subquery = fts_search_subquery(query)
from( trigram_subquery = trigram_search_subquery(query)
s in subquery(subquery), union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
order_by: [desc: s.search_rank], distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
limit: ^(options[:limit] || 20)
)
results = from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
q order_by: [desc: s.search_rank],
|> Repo.all() limit: 20
|> Enum.filter(&(&1.search_rank > 0)) )
end
boost_search_results(results, for_user) defp boost_search_rank_query(query, nil), do: query
defp boost_search_rank_query(query, for_user) do
friends_ids = get_friends_ids(for_user)
followers_ids = get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
CASE WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.2
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.search_rank
)
}
)
end end
defp fts_search_subquery(term, query \\ User) do defp fts_search_subquery(term, query \\ User) do
@ -864,6 +885,7 @@ defp fts_search_subquery(term, query \\ User) do
from( from(
u in query, u in query,
select_merge: %{ select_merge: %{
search_type: ^0,
search_rank: search_rank:
fragment( fragment(
""" """
@ -896,6 +918,8 @@ defp trigram_search_subquery(term) do
from( from(
u in User, u in User,
select_merge: %{ select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank: search_rank:
fragment( fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))", "similarity(?, trim(? || ' ' || coalesce(?, '')))",
@ -908,33 +932,6 @@ defp trigram_search_subquery(term) do
) )
end end
defp boost_search_results(results, nil), do: results
defp boost_search_results(results, for_user) do
friends_ids = get_friends_ids(for_user)
followers_ids = get_followers_ids(for_user)
Enum.map(
results,
fn u ->
search_rank_coef =
cond do
u.id in friends_ids ->
1.2
u.id in followers_ids ->
1.1
true ->
1
end
Map.put(u, :search_rank, u.search_rank * search_rank_coef)
end
)
|> Enum.sort_by(&(-&1.search_rank))
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map( Enum.map(
blocked_identifiers, blocked_identifiers,
@ -1113,13 +1110,15 @@ def delete(%User{} = user) do
friends friends
|> Enum.each(fn followed -> User.unfollow(user, followed) end) |> Enum.each(fn followed -> User.unfollow(user, followed) end)
query = from(a in Activity, where: a.actor == ^user.ap_id) query =
from(a in Activity, where: a.actor == ^user.ap_id)
|> Activity.with_preloaded_object()
Repo.all(query) Repo.all(query)
|> Enum.each(fn activity -> |> Enum.each(fn activity ->
case activity.data["type"] do case activity.data["type"] do
"Create" -> "Create" ->
ActivityPub.delete(Object.normalize(activity.data["object"])) ActivityPub.delete(Object.normalize(activity))
# TODO: Do something with likes, follows, repeats. # TODO: Do something with likes, follows, repeats.
_ -> _ ->
@ -1159,9 +1158,12 @@ def get_or_fetch_by_ap_id(ap_id) do
if !is_nil(user) and !User.needs_update?(user) do if !is_nil(user) and !User.needs_update?(user) do
user user
else else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
user = fetch_by_ap_id(ap_id) user = fetch_by_ap_id(ap_id)
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do if should_fetch_initial do
with %User{} = user do with %User{} = user do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end end

View file

@ -95,7 +95,7 @@ def insert(map, local \\ true) when is_map(map) do
:ok <- check_actor_is_active(map["actor"]), :ok <- check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)}, {_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map), {:ok, map} <- MRF.filter(map),
:ok <- insert_full_object(map) do {:ok, object} <- insert_full_object(map) do
{recipients, _, _} = get_recipients(map) {recipients, _, _} = get_recipients(map)
{:ok, activity} = {:ok, activity} =
@ -106,6 +106,14 @@ def insert(map, local \\ true) when is_map(map) do
recipients: recipients recipients: recipients
}) })
# Splice in the child object if we have one.
activity =
if !is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
Task.start(fn -> Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end) end)
@ -430,6 +438,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do
), ),
order_by: [desc: :id] order_by: [desc: :id]
) )
|> Activity.with_preloaded_object()
Repo.all(query) Repo.all(query)
end end
@ -709,6 +718,13 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
defp restrict_muted_reblogs(query, _), do: query defp restrict_muted_reblogs(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = base_query =
from( from(
@ -718,6 +734,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
) )
base_query base_query
|> maybe_preload_objects(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts) |> restrict_tag_reject(opts)
@ -940,7 +957,7 @@ def fetch_object_from_id(id) do
}, },
:ok <- Transmogrifier.contain_origin(id, params), :ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity.data["object"])} {:ok, Object.normalize(activity)}
else else
{:error, {:reject, nil}} -> {:error, {:reject, nil}} ->
{:reject, nil} {:reject, nil}
@ -952,7 +969,7 @@ def fetch_object_from_id(id) do
Logger.info("Couldn't get object via AP, trying out OStatus fetching...") Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"])} {:ok, [activity | _]} -> {:ok, Object.normalize(activity)}
e -> e e -> e
end end
end end

View file

@ -4,6 +4,10 @@
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp string_matches?(string, _) when not is_binary(string) do
false
end
defp string_matches?(string, pattern) when is_binary(pattern) do defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern) String.contains?(string, pattern)
end end
@ -44,6 +48,20 @@ defp check_ftl_removal(
end end
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
content =
if is_binary(content) do
content
else
""
end
summary =
if is_binary(summary) do
summary
else
""
end
{content, summary} = {content, summary} =
Enum.reduce( Enum.reduce(
Pleroma.Config.get([:mrf_keyword, :replace]), Pleroma.Config.get([:mrf_keyword, :replace]),
@ -60,11 +78,6 @@ defp check_replace(%{"object" => %{"content" => content, "summary" => summary}}
|> put_in(["object", "summary"], summary)} |> put_in(["object", "summary"], summary)}
end end
@impl true
def filter(%{"object" => %{"content" => nil}} = message) do
{:ok, message}
end
@impl true @impl true
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
with {:ok, message} <- check_reject(message), with {:ok, message} <- check_reject(message),

View file

@ -41,7 +41,7 @@ def unfollow(target_instance) do
def publish(%Activity{data: %{"type" => "Create"}} = activity) do def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(), with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity.data["object"]["id"]) do %Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false) ActivityPub.announce(user, object, nil, true, false)
else else
e -> Logger.error("error: #{inspect(e)}") e -> Logger.error("error: #{inspect(e)}")

View file

@ -86,11 +86,15 @@ def fix_object(object) do
end end
def fix_addressing_list(map, field) do def fix_addressing_list(map, field) do
if is_binary(map[field]) do cond do
map is_binary(map[field]) ->
|> Map.put(field, [map[field]]) Map.put(map, field, [map[field]])
else
map is_nil(map[field]) ->
Map.put(map, field, [])
true ->
map
end end
end end
@ -128,13 +132,42 @@ def fix_explicit_addressing(object) do
|> fix_explicit_addressing(explicit_mentions) |> fix_explicit_addressing(explicit_mentions)
end end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
"https://www.w3.org/ns/activitystreams#Public" in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
"https://www.w3.org/ns/activitystreams#Public" in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do def fix_addressing(object) do
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)
object object
|> fix_addressing_list("to") |> fix_addressing_list("to")
|> fix_addressing_list("cc") |> fix_addressing_list("cc")
|> fix_addressing_list("bto") |> fix_addressing_list("bto")
|> fix_addressing_list("bcc") |> fix_addressing_list("bcc")
|> fix_explicit_addressing |> fix_explicit_addressing
|> fix_implicit_addressing(followers_collection)
end end
def fix_actor(%{"attributedTo" => actor} = object) do def fix_actor(%{"attributedTo" => actor} = object) do
@ -922,7 +955,8 @@ defp strip_internal_tags(%{"tag" => tags} = object) do
defp strip_internal_tags(object), do: object defp strip_internal_tags(object), do: object
defp user_upgrade_task(user) do defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user) # we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q = q =
from( from(

View file

@ -209,12 +209,12 @@ def lazy_put_object_defaults(map, activity \\ %{}) do
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in @supported_object_types do when is_map(object_data) and type in @supported_object_types do
with {:ok, _} <- Object.create(object_data) do with {:ok, object} <- Object.create(object_data) do
:ok {:ok, object}
end end
end end
def insert_full_object(_), do: :ok def insert_full_object(_), do: {:ok, nil}
def update_object_in_activities(%{data: %{"id" => id}} = object) do def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO # TODO

View file

@ -17,7 +17,7 @@ def render("object.json", %{object: %Object{} = object}) do
def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
additional = additional =
Transmogrifier.prepare_object(activity.data) Transmogrifier.prepare_object(activity.data)
@ -28,7 +28,7 @@ def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = act
def render("object.json", %{object: %Activity{} = activity}) do def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
additional = additional =
Transmogrifier.prepare_object(activity.data) Transmogrifier.prepare_object(activity.data)

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -64,8 +63,9 @@ def reject_follow_request(follower, followed) do
end end
def delete(activity_id, user) do def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), with %Activity{data: %{"object" => _}} = activity <-
%Object{} = object <- Object.normalize(object_id), Activity.get_by_id_with_object(activity_id),
%Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user), {:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
@ -75,7 +75,7 @@ def delete(activity_id, user) do
def repeat(id_or_ap_id, user) do def repeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]), object <- Object.normalize(activity),
nil <- Utils.get_existing_announce(user.ap_id, object) do nil <- Utils.get_existing_announce(user.ap_id, object) do
ActivityPub.announce(user, object) ActivityPub.announce(user, object)
else else
@ -86,7 +86,7 @@ def repeat(id_or_ap_id, user) do
def unrepeat(id_or_ap_id, user) do def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]) do object <- Object.normalize(activity) do
ActivityPub.unannounce(user, object) ActivityPub.unannounce(user, object)
else else
_ -> _ ->
@ -96,7 +96,7 @@ def unrepeat(id_or_ap_id, user) do
def favorite(id_or_ap_id, user) do def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]), object <- Object.normalize(activity),
nil <- Utils.get_existing_like(user.ap_id, object) do nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object) ActivityPub.like(user, object)
else else
@ -107,7 +107,7 @@ def favorite(id_or_ap_id, user) do
def unfavorite(id_or_ap_id, user) do def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]) do object <- Object.normalize(activity) do
ActivityPub.unlike(user, object) ActivityPub.unlike(user, object)
else else
_ -> _ ->
@ -142,7 +142,8 @@ def post(user, %{"status" => status} = data) do
make_content_html( make_content_html(
status, status,
attachments, attachments,
data data,
visibility
), ),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to), context <- make_context(in_reply_to),

View file

@ -17,13 +17,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
# This is a hack for twidere. # This is a hack for twidere.
def get_by_id_or_ap_id(id) do def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id) activity =
Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
activity && activity &&
if activity.data["type"] == "Create" do if activity.data["type"] == "Create" do
activity activity
else else
Activity.get_create_by_object_ap_id(activity.data["object"]) Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end end
end end
@ -101,7 +102,8 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
def make_content_html( def make_content_html(
status, status,
attachments, attachments,
data data,
visibility
) do ) do
no_attachment_links = no_attachment_links =
data data
@ -110,8 +112,15 @@ def make_content_html(
content_type = get_content_type(data["content_type"]) content_type = get_content_type(data["content_type"])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
status status
|> format_input(content_type) |> format_input(content_type, options)
|> maybe_add_attachments(attachments, no_attachment_links) |> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data) |> maybe_add_nsfw_tag(data)
end end
@ -294,10 +303,10 @@ def maybe_notify_to_recipients(
def maybe_notify_mentioned_recipients( def maybe_notify_mentioned_recipients(
recipients, recipients,
%Activity{data: %{"to" => _to, "type" => type} = data} = _activity %Activity{data: %{"to" => _to, "type" => type} = data} = activity
) )
when type == "Create" do when type == "Create" do
object = Object.normalize(data["object"]) object = Object.normalize(activity)
object_data = object_data =
cond do cond do
@ -344,4 +353,33 @@ def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
end end
def get_report_statuses(_, _), do: {:ok, nil} def get_report_statuses(_, _), do: {:ok, nil}
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
end end

View file

@ -2,61 +2,49 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.Repo alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.User alias Pleroma.User
@default_limit 20
def get_followers(user, params \\ %{}) do def get_followers(user, params \\ %{}) do
user user
|> User.get_followers_query() |> User.get_followers_query()
|> paginate(params) |> Pagination.fetch_paginated(params)
|> Repo.all()
end end
def get_friends(user, params \\ %{}) do def get_friends(user, params \\ %{}) do
user user
|> User.get_friends_query() |> User.get_friends_query()
|> paginate(params) |> Pagination.fetch_paginated(params)
|> Repo.all()
end end
def paginate(query, params \\ %{}) do def get_notifications(user, params \\ %{}) do
options = cast_params(params) options = cast_params(params)
query user
|> restrict(:max_id, options) |> Notification.for_user_query()
|> restrict(:since_id, options) |> restrict(:exclude_types, options)
|> restrict(:limit, options) |> Pagination.fetch_paginated(params)
|> order_by([u], fragment("? desc nulls last", u.id))
end end
def cast_params(params) do defp cast_params(params) do
param_types = %{ param_types = %{
max_id: :string, exclude_types: {:array, :string}
since_id: :string,
limit: :integer
} }
changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes changeset.changes
end end
defp restrict(query, :max_id, %{max_id: max_id}) do defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
query ap_types =
|> where([q], q.id < ^max_id) mastodon_types
end |> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
defp restrict(query, :since_id, %{since_id: since_id}) do
query
|> where([q], q.id > ^since_id)
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)
query query
|> limit(^limit) |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end end
defp restrict(query, _, _), do: query defp restrict(query, _, _), do: query

View file

@ -502,7 +502,7 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def notifications(%{assigns: %{user: user}} = conn, params) do def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) notifications = MastodonAPI.get_notifications(user, params)
conn conn
|> add_link_headers(:notifications, notifications) |> add_link_headers(:notifications, notifications)
@ -944,12 +944,14 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
end end
def favourites(%{assigns: %{user: user}} = conn, params) do def favourites(%{assigns: %{user: user}} = conn, params) do
activities = params =
params params
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id) |> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> ActivityPub.fetch_public_activities()
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse() |> Enum.reverse()
conn conn

View file

@ -46,6 +46,14 @@ defp get_user(ap_id) do
end end
end end
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
do: Utils.context_to_conversation_id(context)
defp get_context_id(_), do: nil
def render("index.json", opts) do def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities) replied_to_activities = get_replied_to_activities(opts.activities)
@ -186,7 +194,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
language: nil, language: nil,
emojis: build_emojis(activity.data["object"]["emoji"]), emojis: build_emojis(activity.data["object"]["emoji"]),
pleroma: %{ pleroma: %{
local: activity.local local: activity.local,
conversation_id: get_context_id(activity)
} }
} }
end end

View file

@ -124,6 +124,9 @@ def raw_nodeinfo do
end, end,
if Keyword.get(instance, :allow_relay) do if Keyword.get(instance, :allow_relay) do
"relay" "relay"
end,
if Keyword.get(instance, :safe_dm_mentions) do
"safe_dm_mentions"
end end
] ]
|> Enum.filter(& &1) |> Enum.filter(& &1)

View file

@ -16,7 +16,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
schema "oauth_authorizations" do schema "oauth_authorizations" do
field(:token, :string) field(:token, :string)
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime) field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false) field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App) belongs_to(:app, App)

View file

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

View file

@ -106,7 +106,7 @@ def fetch_replied_to_activity(entry, in_reply_to) do
# TODO: Clean this up a bit. # TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id(id), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
[author] <- :xmerl_xpath.string('//author[1]', doc), [author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author), {:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry), content_html <- OStatus.get_content(entry),

View file

@ -23,8 +23,8 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub alias Pleroma.Web.Websub
def is_representable?(%Activity{data: data}) do def is_representable?(%Activity{} = activity) do
object = Object.normalize(data["object"]) object = Object.normalize(activity)
cond do cond do
is_nil(object) -> is_nil(object) ->
@ -119,7 +119,7 @@ def handle_incoming(xml_string) do
def make_share(entry, doc, retweeted_activity) do def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc), with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(retweeted_activity.data["object"]), %Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry), id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -137,7 +137,7 @@ def handle_share(entry, doc) do
def make_favorite(entry, doc, favorited_activity) do def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc), with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(favorited_activity.data["object"]), %Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry), id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -159,7 +159,7 @@ def get_or_try_fetching(entry) do
Logger.debug("Trying to get entry from db") Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry), with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
{:ok, activity} {:ok, activity}
else else
_ -> _ ->

View file

@ -102,7 +102,8 @@ def object(conn, %{"uuid" => uuid}) do
ActivityPubController.call(conn, :object) ActivityPubController.call(conn, :object)
else else
with id <- o_status_url(conn, :object, uuid), with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)}, {_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)}, {_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do case get_format(conn) do
@ -148,13 +149,13 @@ def activity(conn, %{"uuid" => uuid}) do
end end
def notice(conn, %{"id" => id}) do def notice(conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)}, with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)}, {_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do case format = get_format(conn) do
"html" -> "html" ->
if activity.data["type"] == "Create" do if activity.data["type"] == "Create" do
%Object{} = object = Object.normalize(activity.data["object"]) %Object{} = object = Object.normalize(activity)
Fallback.RedirectController.redirector_with_meta(conn, %{ Fallback.RedirectController.redirector_with_meta(conn, %{
activity_id: activity.id, activity_id: activity.id,
@ -191,9 +192,9 @@ def notice(conn, %{"id" => id}) do
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes. # Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
%Object{} = object <- Object.normalize(activity.data["object"]), %Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn conn
@ -219,7 +220,7 @@ defp represent_activity(
%Activity{data: %{"type" => "Create"}} = activity, %Activity{data: %{"type" => "Create"}} = activity,
_user _user
) do ) do
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")

View file

@ -21,9 +21,9 @@ defp validate_page_url(%URI{scheme: nil}), do: :error
defp validate_page_url(%URI{}), do: :ok defp validate_page_url(%URI{}), do: :ok
defp validate_page_url(_), do: :error defp validate_page_url(_), do: :error
def fetch_data_for_activity(%Activity{} = activity) do def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- Pleroma.Config.get([:rich_media, :enabled]), with true <- Pleroma.Config.get([:rich_media, :enabled]),
%Object{} = object <- Object.normalize(activity.data["object"]), %Object{} = object <- Object.normalize(activity),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
:ok <- validate_page_url(page_url), :ok <- validate_page_url(page_url),
{:ok, rich_media} <- Parser.parse(page_url) do {:ok, rich_media} <- Parser.parse(page_url) do
@ -32,4 +32,6 @@ def fetch_data_for_activity(%Activity{} = activity) do
_ -> %{} _ -> %{}
end end
end end
def fetch_data_for_activity(_), do: %{}
end end

View file

@ -202,7 +202,7 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite
mutes = user.info.mutes || [] mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || [] reblog_mutes = user.info.muted_reblogs || []
parent = Object.normalize(item.data["object"]) parent = Object.normalize(item)
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or

View file

@ -197,7 +197,9 @@ def config(conn, _params) do
vapidPublicKey: vapid_public_key, vapidPublicKey: vapid_public_key,
accountActivationRequired: accountActivationRequired:
if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"), if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0") invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"),
safeDMMentionsEnabled:
if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
} }
pleroma_fe = pleroma_fe =

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Mailer alias Pleroma.Mailer
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserEmail alias Pleroma.UserEmail
@ -282,35 +281,6 @@ def search(_user, %{"q" => query} = params) do
_activities = Repo.all(q) _activities = Repo.all(q)
end end
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
def get_external_profile(for_user, uri) do def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do with %User{} = user <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})} {:ok, UserView.render("show.json", %{user: user, for: for_user})}

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.NotificationView alias Pleroma.Web.TwitterAPI.NotificationView
@ -278,7 +279,7 @@ def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id), with context when is_binary(context) <- Utils.conversation_id_to_context(id),
activities <- activities <-
ActivityPub.fetch_activities_for_context(context, %{ ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user, "blocking_user" => user,

View file

@ -15,7 +15,6 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.TwitterAPI.UserView
import Ecto.Query import Ecto.Query
@ -78,7 +77,7 @@ defp get_context_id(%{data: %{"context" => nil}}, _), do: nil
defp get_context_id(%{data: %{"context" => context}}, options) do defp get_context_id(%{data: %{"context" => context}}, options) do
cond do cond do
id = options[:context_ids][context] -> id id = options[:context_ids][context] -> id
true -> TwitterAPI.context_to_conversation_id(context) true -> Utils.context_to_conversation_id(context)
end end
end end
@ -267,6 +266,8 @@ def render(
content content
|> String.replace(~r/<br\s?\/?>/, "\n") |> String.replace(~r/<br\s?\/?>/, "\n")
|> HTML.get_cached_stripped_html_for_object(activity, __MODULE__) |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
else
""
end end
reply_parent = Activity.get_in_reply_to_activity(activity) reply_parent = Activity.get_in_reply_to_activity(activity)

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
schema "websub_client_subscriptions" do schema "websub_client_subscriptions" do
field(:topic, :string) field(:topic, :string)
field(:secret, :string) field(:secret, :string)
field(:valid_until, :naive_datetime) field(:valid_until, :naive_datetime_usec)
field(:state, :string) field(:state, :string)
field(:subscribers, {:array, :string}, default: []) field(:subscribers, {:array, :string}, default: [])
field(:hub, :string) field(:hub, :string)

View file

@ -9,6 +9,7 @@ def project do
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(), compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: true], elixirc_options: [warnings_as_errors: true],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
@ -61,7 +62,8 @@ defp deps do
{:phoenix, "~> 1.4.1"}, {:phoenix, "~> 1.4.1"},
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"}, {:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 3.3"}, {:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~>3.0.5"},
{:postgrex, ">= 0.13.5"}, {:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.15"}, {:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"}, {:comeonin, "~> 4.1.1"},

View file

@ -13,10 +13,11 @@
"cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "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"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "2.0.5", "ddb2ba6761a08b2bb9ca0e7d260e8f4dd39067426d835c24491a321b7f92a4da", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
@ -46,7 +47,7 @@
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
@ -54,11 +55,12 @@
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [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]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [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]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
"telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},

View file

@ -5,7 +5,7 @@ def change do
create table(:websub_client_subscriptions) do create table(:websub_client_subscriptions) do
add :topic, :string add :topic, :string
add :secret, :string add :secret, :string
add :valid_until, :naive_datetime add :valid_until, :naive_datetime_usec
add :state, :string add :state, :string
add :subscribers, :map add :subscribers, :map

View file

@ -6,7 +6,7 @@ def change do
add :app_id, references(:apps) add :app_id, references(:apps)
add :user_id, references(:users) add :user_id, references(:users)
add :token, :string add :token, :string
add :valid_until, :naive_datetime add :valid_until, :naive_datetime_usec
add :used, :boolean, default: false add :used, :boolean, default: false
timestamps() timestamps()

View file

@ -7,7 +7,7 @@ def change do
add :user_id, references(:users) add :user_id, references(:users)
add :token, :string add :token, :string
add :refresh_token, :string add :refresh_token, :string
add :valid_until, :naive_datetime add :valid_until, :naive_datetime_usec
timestamps() timestamps()
end end

View file

@ -8,7 +8,7 @@ def change do
add :hide, :boolean add :hide, :boolean
add :phrase, :string add :phrase, :string
add :context, {:array, :string} add :context, {:array, :string}
add :expires_at, :datetime add :expires_at, :utc_datetime
add :whole_word, :boolean add :whole_word, :boolean
timestamps() timestamps()

View file

@ -3,7 +3,7 @@ defmodule Pleroma.Repo.Migrations.UsersAddLastRefreshedAt do
def change do def change do
alter table(:users) do alter table(:users) do
add :last_refreshed_at, :naive_datetime add :last_refreshed_at, :naive_datetime_usec
end end
end end
end end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.CreateInstances do
def change do def change do
create table(:instances) do create table(:instances) do
add :host, :string add :host, :string
add :unreachable_since, :naive_datetime add :unreachable_since, :naive_datetime_usec
timestamps() timestamps()
end end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 236 KiB

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