Merge remote-tracking branch 'origin/develop' into shigusegubu

* origin/develop: (62 commits)
  update pleroma frontend
  tests: add regression test
  MRF: anti followbot: gracefully handle accounts without a display name
  Adjust delete activity audience to match the deleted object
  Revert existing object check
  Preserve parameters in link headers (Mastodon API)
  [#647] tests for web push
  Keep heading short
  Add default config for masto_fe
  Add handling of objects not in database
  Add tests for reserved char escaping in upload
  Properly escape reserved URI charachters in upload urls
  Web.RelMe: Fix having other values in rel attr
  Plugs.HTTPSecurityPlug: Add static_url to CSP's connect-src
  Include admins in nodeinfo
  Typo
  rich media: helpers: rework validate_page_url()
  local -> only_local
  Format & update docs
  helpers: use AutoLinker to validate URIs as well as the other tests
  ...
This commit is contained in:
Henry Jameson 2019-03-07 19:54:48 +02:00
commit 66185ad957
71 changed files with 1860 additions and 464 deletions

View file

@ -34,13 +34,16 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = String.replace(static_url, "http", "ws")
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
connect_src =
if Mix.env() == :dev do
"connect-src 'self' http://localhost:3035/ " <> websocket_url
connect_src <> " http://localhost:3035/"
else
"connect-src 'self' " <> websocket_url
connect_src
end
script_src =

View file

@ -85,6 +85,10 @@ defmodule Pleroma.Upload do
end
end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
@ -218,9 +222,7 @@ defmodule Pleroma.Upload do
defp url_from_spec(base_url, {:file, path}) do
path =
path
|> URI.encode()
|> String.replace("?", "%3F")
|> String.replace(":", "%3A")
|> URI.encode(&char_unescaped?/1)
[base_url, "media", path]
|> Path.join()

View file

@ -22,6 +22,7 @@ defmodule Pleroma.User do
alias Pleroma.Web.OAuth
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.RelMe
require Logger
@ -547,11 +548,8 @@ defmodule Pleroma.User do
end
def get_followers_query(user, page) do
from(
u in get_followers_query(user, nil),
limit: 20,
offset: ^((page - 1) * 20)
)
from(u in get_followers_query(user, nil))
|> paginate(page, 20)
end
def get_followers_query(user), do: get_followers_query(user, nil)
@ -577,11 +575,8 @@ defmodule Pleroma.User do
end
def get_friends_query(user, page) do
from(
u in get_friends_query(user, nil),
limit: 20,
offset: ^((page - 1) * 20)
)
from(u in get_friends_query(user, nil))
|> paginate(page, 20)
end
def get_friends_query(user), do: get_friends_query(user, nil)
@ -621,64 +616,57 @@ defmodule Pleroma.User do
)
end
def update_follow_request_count(%User{} = user) do
subquery =
def get_follow_requests(%User{} = user) do
users =
user
|> User.get_follow_requests_query()
|> select([a], %{count: count(a.id)})
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(subquery))
|> update([u, s],
set: [
info:
fragment(
"jsonb_set(?, '{follow_request_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> {:ok, user}
_ -> {:error, user}
end
end
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
users =
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !is_nil(u) end)
|> Enum.filter(fn u -> !following?(u, user) end)
|> join(:inner, [a], u in User, 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
info_cng = User.Info.add_to_note_count(user.info, 1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
User
|> where(id: ^user.id)
|> update([u],
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def decrease_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, -1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
User
|> where(id: ^user.id)
|> update([u],
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def update_note_count(%User{} = user) do
@ -702,24 +690,29 @@ defmodule Pleroma.User do
def update_follower_count(%User{} = user) do
follower_count_query =
from(
u in User,
where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
)
User
|> where([u], ^user.follower_address in u.following)
|> where([u], u.id != ^user.id)
|> select([u], %{count: count(u.id)})
follower_count = Repo.one(follower_count_query)
info_cng =
user.info
|> User.Info.set_follower_count(follower_count)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(follower_count_query))
|> update([u, s],
set: [
info:
fragment(
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def get_users_from_set_query(ap_ids, false) do
@ -756,6 +749,59 @@ defmodule Pleroma.User do
Repo.all(query)
end
@spec search_for_admin(%{
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
query =
from(u in User, order_by: u.id)
|> maybe_local_user_query(local)
paginated_query =
query
|> paginate(page, page_size)
count =
query
|> Repo.aggregate(:count, :id)
{:ok, Repo.all(paginated_query), count}
end
@spec search_for_admin(%{
query: binary(),
admin: Pleroma.User.t(),
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{
query: term,
admin: admin,
local: local,
page: page,
page_size: page_size
}) do
term = String.trim_leading(term, "@")
local_paginated_query =
User
|> maybe_local_user_query(local)
|> paginate(page, page_size)
search_query = fts_search_subquery(term, local_paginated_query)
count =
term
|> fts_search_subquery()
|> maybe_local_user_query(local)
|> Repo.aggregate(:count, :id)
{:ok, do_search(search_query, admin), count}
end
def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
@ -773,12 +819,6 @@ defmodule Pleroma.User do
Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
end
def all_except_one(user) do
query = from(u in User, where: u.id != ^user.id)
Repo.all(query)
end
defp do_search(subquery, for_user, options \\ []) do
q =
from(
@ -795,9 +835,9 @@ defmodule Pleroma.User do
boost_search_results(results, for_user)
end
defp fts_search_subquery(query) do
defp fts_search_subquery(term, query \\ User) do
processed_query =
query
term
|> String.replace(~r/\W+/, " ")
|> String.trim()
|> String.split()
@ -805,7 +845,7 @@ defmodule Pleroma.User do
|> Enum.join(" | ")
from(
u in User,
u in query,
select_merge: %{
search_rank:
fragment(
@ -835,19 +875,19 @@ defmodule Pleroma.User do
)
end
defp trigram_search_subquery(query) do
defp trigram_search_subquery(term) do
from(
u in User,
select_merge: %{
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^query,
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
end
@ -1005,9 +1045,13 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
def local_user_query do
def maybe_local_user_query(query, local) do
if local, do: local_user_query(query), else: query
end
def local_user_query(query \\ User) do
from(
u in User,
u in query,
where: u.local == true,
where: not is_nil(u.nickname)
)
@ -1202,8 +1246,14 @@ defmodule Pleroma.User do
{String.trim(name, ":"), url}
end)
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
|> Formatter.emojify(emoji)
end
@ -1299,4 +1349,11 @@ defmodule Pleroma.User do
)
|> Repo.all()
end
defp paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
end

View file

@ -12,7 +12,6 @@ defmodule Pleroma.User.Info do
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:follow_request_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)

View file

@ -81,6 +81,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map),
@ -163,7 +171,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
),
{:ok, activity} <- insert(create_data, local),
# Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
{:ok, _actor} <- User.increase_note_count(actor),
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@ -175,8 +183,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(actor) do
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@ -187,8 +194,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(actor) do
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@ -286,8 +292,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(followed) do
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@ -297,26 +302,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(followed) do
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
to = object.data["to"] || [] ++ object.data["cc"] || []
data = %{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
"to" => to
}
with {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local),
# Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
{:ok, _actor} <- User.decrease_note_count(user),
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@ -420,6 +425,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
query =

View file

@ -23,15 +23,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
defp score_displayname(_), do: 0.0
defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
# nickname will always be a binary string because it's generated by Pleroma.
nick_score =
nickname
|> String.downcase()
|> score_nickname()
# displayname will either be a binary string or nil, if a displayname isn't set.
name_score =
displayname
|> String.downcase()
|> score_displayname()
if is_binary(displayname) do
displayname
|> String.downcase()
|> score_displayname()
else
0.0
end
nick_score + name_score
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Web
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Web.Router.Helpers
@ -274,13 +275,31 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Repo.all(query)
end
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
def make_like_data(
%User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
activity_id
) do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
data = %{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => [actor.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}

View file

@ -3,10 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@users_page_size 50
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.MastodonAPI.Admin.AccountView
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -48,7 +50,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
conn
|> json(UserView.render("show_for_admin.json", %{user: updated_user}))
|> json(AccountView.render("show.json", %{user: updated_user}))
end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
@ -61,11 +63,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
do: json_response(conn, :no_content, "")
end
def list_users(%{assigns: %{user: admin}} = conn, _data) do
users = User.all_except_one(admin)
def list_users(%{assigns: %{user: admin}} = conn, params) do
{page, page_size} = page_params(params)
conn
|> json(UserView.render("index_for_admin.json", %{users: users}))
with {:ok, users, count} <-
User.search_for_admin(%{
query: params["query"],
admin: admin,
local: params["local_only"] == "true",
page: page,
page_size: page_size
}),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
@ -211,4 +228,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> put_status(500)
|> json("Something went wrong")
end
defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])}
end
defp get_page(page_string) when is_nil(page_string), do: 1
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end
end

View file

@ -14,6 +14,19 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
def follow(follower, followed) do
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
end
end
def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id),

View file

@ -15,14 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Push
alias Push.Subscription
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.PushSubscriptionView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.ActivityPub.ActivityPub
@ -193,6 +190,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id"])
|> Map.merge(params)
last = List.last(activities)
first = List.first(activities)
@ -292,13 +294,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", visibility: "direct"})
)
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
activities = Repo.all(query)
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Repo.all()
conn
|> add_link_headers(:dm_timeline, activities)
@ -733,14 +739,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
@ -754,8 +753,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("account.json", %{user: followed, for: follower})
@ -894,7 +892,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user)
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query)
@ -919,7 +917,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user)
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query)
@ -941,7 +939,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user)
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
@ -1424,37 +1422,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
true = Push.enabled()
Subscription.delete_if_exists(user, token)
{:ok, subscription} = Subscription.create(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Push.enabled()
subscription = Subscription.get(user, token)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def update_push_subscription(
%{assigns: %{user: user, token: token}} = conn,
params
) do
true = Push.enabled()
{:ok, subscription} = Subscription.update(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Push.enabled()
{:ok, _response} = Subscription.delete(user, token)
json(conn, %{})
end
# fallback action
#
def errors(conn, _) do
conn
|> put_status(500)

View file

@ -0,0 +1,71 @@
# 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.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
action_fallback(:errors)
# Creates PushSubscription
# POST /api/v1/push/subscription
#
def create(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Gets PushSubscription
# GET /api/v1/push/subscription
#
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.get(user, token) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
def update(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.update(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Deletes PushSubscription
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
# fallback action
#
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end

View file

@ -0,0 +1,25 @@
# 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.Admin.AccountView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.Admin.AccountView
def render("index.json", %{users: users, count: count, page_size: page_size}) do
%{
users: render_many(users, AccountView, "show.json", as: :user),
count: count,
page_size: page_size
}
end
def render("show.json", %{user: user}) do
%{
"id" => user.id,
"nickname" => user.nickname,
"deactivated" => user.info.deactivated
}
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Push
def render("push_subscription.json", %{subscription: subscription}) do
%{
@ -14,7 +15,5 @@ defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
}
end
defp server_key do
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
end
defp server_key, do: Keyword.get(Push.vapid_config(), :public_key)
end

View file

@ -17,14 +17,14 @@ defmodule Pleroma.Web.Metadata.Utils do
|> Formatter.truncate()
end
def scrub_html_and_truncate(content) when is_binary(content) do
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
|> Formatter.demojify()
|> Formatter.truncate()
|> Formatter.truncate(max_length)
end
def attachment_url(url) do

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@ -86,8 +85,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
end
staff_accounts =
User.moderator_user_query()
|> Repo.all()
User.all_superusers()
|> Enum.map(fn u -> u.ap_id end)
mrf_user_allowlist =

View file

@ -0,0 +1,127 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Impl do
@moduledoc "The module represents implementation push web notification"
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.Push.Subscription
alias Pleroma.Web.Metadata.Utils
alias Pleroma.Notification
require Logger
import Ecto.Query
@types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions"
@spec perform_send(Notification.t()) :: list(any)
def perform_send(%{activity: %{data: %{"type" => activity_type}}, user_id: user_id} = notif)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
body: format_body(notif, actor),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
preferred_locale: "en"
}
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
end
def perform_send(_) do
Logger.warn("Unknown notification type")
:error
end
@doc "Push message to web"
def push_message(body, sub, api_key, subscription) do
case WebPushEncryption.send_web_push(body, sub, api_key) do
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
:ok
{:ok, %{status_code: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
_ ->
Logger.error("Web Push Notification failed with unknown error")
:error
end
end
@doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
|> Repo.all()
end
def build_sub(subscription) do
%{
keys: %{
p256dh: subscription.key_p256dh,
auth: subscription.key_auth
},
endpoint: subscription.endpoint
}
end
def format_body(
%{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}},
actor
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce", "object" => activity_id}}},
actor
) do
%Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
%Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
actor
)
when type in ["Follow", "Like"] do
case type do
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post"
end
end
def format_title(%{activity: %{data: %{"type" => type}}}) do
case type do
"Create" -> "New Mention"
"Follow" -> "New Follower"
"Announce" -> "New Repeat"
"Like" -> "New Favorite"
end
end
end

View file

@ -5,14 +5,13 @@
defmodule Pleroma.Web.Push do
use GenServer
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Push.Subscription
alias Pleroma.Web.Push.Impl
require Logger
import Ecto.Query
@types ["Create", "Follow", "Announce", "Like"]
##############
# Client API #
##############
def start_link() do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
@ -30,14 +29,18 @@ defmodule Pleroma.Web.Push do
end
end
def send(notification) do
if enabled() do
GenServer.cast(Pleroma.Web.Push, {:send, notification})
end
end
def send(notification),
do: GenServer.cast(__MODULE__, {:send, notification})
####################
# Server Callbacks #
####################
@impl true
def init(:ok) do
if !enabled() do
if enabled() do
{:ok, nil}
else
Logger.warn("""
VAPID key pair is not found. If you wish to enabled web push, please run
@ -47,93 +50,15 @@ defmodule Pleroma.Web.Push do
""")
:ignore
else
{:ok, nil}
end
end
def handle_cast(
{:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification},
state
)
when type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
type = Pleroma.Activity.mastodon_notification_type(notification.activity)
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
|> Repo.all()
|> Enum.filter(fn subscription ->
get_in(subscription.data, ["alerts", type]) || false
end)
|> Enum.each(fn subscription ->
sub = %{
keys: %{
p256dh: subscription.key_p256dh,
auth: subscription.key_auth
},
endpoint: subscription.endpoint
}
body =
Jason.encode!(%{
title: format_title(notification),
access_token: subscription.token.token,
body: format_body(notification, actor),
notification_id: notification.id,
notification_type: type,
icon: User.avatar_url(actor),
preferred_locale: "en"
})
case WebPushEncryption.send_web_push(
body,
sub,
Application.get_env(:web_push_encryption, :gcm_api_key)
) do
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
:ok
{:ok, %{status_code: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
_ ->
Logger.error("Web Push Notification failed with unknown error")
:error
end
end)
@impl true
def handle_cast({:send, notification}, state) do
if enabled() do
Impl.perform_send(notification)
end
{:noreply, state}
end
def handle_cast({:send, _}, state) do
Logger.warn("Unknown notification type")
{:noreply, state}
end
defp format_title(%{activity: %{data: %{"type" => type}}}) do
case type do
"Create" -> "New Mention"
"Follow" -> "New Follower"
"Announce" -> "New Repeat"
"Like" -> "New Favorite"
end
end
defp format_body(%{activity: %{data: %{"type" => type}}}, actor) do
case type do
"Create" -> "@#{actor.nickname} has mentioned you"
"Follow" -> "@#{actor.nickname} has followed you"
"Announce" -> "@#{actor.nickname} has repeated your post"
"Like" -> "@#{actor.nickname} has favorited your post"
end
end
end

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.Push.Subscription do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Push.Subscription
@type t :: %__MODULE__{}
schema "push_subscriptions" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:token, Token)
@ -50,24 +52,32 @@ defmodule Pleroma.Web.Push.Subscription do
})
end
@doc "Gets subsciption by user & token"
@spec get(User.t(), Token.t()) :: {:ok, t()} | {:error, :not_found}
def get(%User{id: user_id}, %Token{id: token_id}) do
Repo.get_by(Subscription, user_id: user_id, token_id: token_id)
case Repo.get_by(Subscription, user_id: user_id, token_id: token_id) do
nil -> {:error, :not_found}
subscription -> {:ok, subscription}
end
end
def update(user, token, params) do
get(user, token)
|> change(data: alerts(params))
|> Repo.update()
with {:ok, subscription} <- get(user, token) do
subscription
|> change(data: alerts(params))
|> Repo.update()
end
end
def delete(user, token) do
Repo.delete(get(user, token))
with {:ok, subscription} <- get(user, token),
do: Repo.delete(subscription)
end
def delete_if_exists(user, token) do
case get(user, token) do
nil -> {:ok, nil}
sub -> Repo.delete(sub)
{:error, _} -> {:ok, nil}
{:ok, sub} -> Repo.delete(sub)
end
end

52
lib/pleroma/web/rel_me.ex Normal file
View file

@ -0,0 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RelMe do
@hackney_options [
pool: :media,
timeout: 2_000,
recv_timeout: 2_000,
max_body: 2_000_000
]
if Mix.env() == :test do
def parse(url) when is_binary(url), do: parse_url(url)
else
def parse(url) when is_binary(url) do
Cachex.fetch!(:rel_me_cache, url, fn _ ->
{:commit, parse_url(url)}
end)
rescue
e -> {:error, "Cachex error: #{inspect(e)}"}
end
end
def parse(_), do: {:error, "No URL provided"}
defp parse_url(url) do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
data =
Floki.attribute(html, "link[rel~=me]", "href") ++
Floki.attribute(html, "a[rel~=me]", "href")
{:ok, data}
rescue
e -> {:error, "Parsing error: #{inspect(e)}"}
end
def maybe_put_rel_me("http" <> _ = target_page, profile_urls) when is_list(profile_urls) do
{:ok, rel_me_hrefs} = parse(target_page)
true = Enum.any?(rel_me_hrefs, fn x -> x in profile_urls end)
"me"
rescue
_ -> nil
end
def maybe_put_rel_me(_, _) do
nil
end
end

View file

@ -8,10 +8,24 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.HTML
alias Pleroma.Web.RichMedia.Parser
defp validate_page_url(page_url) when is_binary(page_url) do
if AutoLinker.Parser.is_url?(page_url, true) do
URI.parse(page_url) |> validate_page_url
else
:error
end
end
defp validate_page_url(%URI{authority: nil}), do: :error
defp validate_page_url(%URI{scheme: nil}), do: :error
defp validate_page_url(%URI{}), do: :ok
defp validate_page_url(_), do: :error
def fetch_data_for_activity(%Activity{} = activity) do
with true <- Pleroma.Config.get([:rich_media, :enabled]),
%Object{} = object <- Object.normalize(activity.data["object"]),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
:ok <- validate_page_url(page_url),
{:ok, rich_media} <- Parser.parse(page_url) do
%{page_url: page_url, rich_media: rich_media}
else

View file

@ -303,10 +303,10 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:oauth_push)
post("/push/subscription", MastodonAPIController, :create_push_subscription)
get("/push/subscription", MastodonAPIController, :get_push_subscription)
put("/push/subscription", MastodonAPIController, :update_push_subscription)
delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
end
end

View file

@ -197,10 +197,12 @@ defmodule Pleroma.Web.Streamer do
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info.blocks || []
mutes = user.info.mutes || []
parent = Object.normalize(item.data["object"])
unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
@ -224,8 +226,9 @@ defmodule Pleroma.Web.Streamer do
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info.blocks || []
mutes = user.info.mutes || []
unless item.actor in blocks do
unless item.actor in blocks or item.actor in mutes do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else

View file

@ -28,18 +28,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
else
err -> err
with {:ok, %User{} = followed} <- get_user(params) do
CommonAPI.follow(follower, followed)
end
end

View file

@ -167,6 +167,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
|> Map.put(:visibility, ~w[unlisted public private])
activities = ActivityPub.fetch_activities([user.ap_id], params)
@ -176,13 +177,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
)
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
activities = Repo.all(query)
activities =
ActivityPub.fetch_activities_query([user.ap_id], params)
|> Repo.all()
conn
|> put_view(ActivityView)
@ -702,7 +706,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, true, user)
users = User.search(query, resolve: true, for_user: user)
conn
|> put_view(UserView)

View file

@ -9,7 +9,6 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.TwitterAPI.UserView
def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
@ -27,19 +26,6 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
else: %{}
end
def render("index_for_admin.json", %{users: users} = opts) do
users
|> render_many(UserView, "show_for_admin.json", opts)
end
def render("show_for_admin.json", %{user: user}) do
%{
"id" => user.id,
"nickname" => user.nickname,
"deactivated" => user.info.deactivated
}
end
def render("short.json", %{
user: %User{
nickname: nickname,
@ -133,7 +119,6 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"tags" => user.tags
}
|> maybe_with_activation_status(user, for_user)
|> maybe_with_follow_request_count(user, for_user)
}
data =
@ -155,14 +140,6 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
defp maybe_with_activation_status(data, _, _), do: data
defp maybe_with_follow_request_count(data, %User{id: id, info: %{locked: true}} = user, %User{
id: id
}) do
Map.put(data, "follow_request_count", user.info.follow_request_count)
end
defp maybe_with_follow_request_count(data, _, _), do: data
defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do
Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role})
end