Resolve merge conflict

This commit is contained in:
Rin Toshaka 2018-12-30 21:00:40 +01:00
commit dec23500d8
56 changed files with 761 additions and 123 deletions

View File

@ -8,6 +8,7 @@ variables:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
MIX_ENV: test
cache:
key: ${CI_COMMIT_REF_SLUG}
@ -23,15 +24,15 @@ before_script:
- mix local.rebar --force
- mix deps.get
- mix compile --force
- MIX_ENV=test mix ecto.create
- MIX_ENV=test mix ecto.migrate
- mix ecto.create
- mix ecto.migrate
lint:
stage: lint
script:
- MIX_ENV=test mix format --check-formatted
- mix format --check-formatted
unit-testing:
stage: test
script:
- MIX_ENV=test mix test --trace
- mix test --trace

View File

@ -30,7 +30,7 @@ While we don't provide docker files, other people have written very good ones. T
### Dependencies
* Postgresql version 9.6 or newer
* Elixir version 1.5 or newer. If your distribution only has an old version available, check [Elixir's install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir's install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools
### Configuration

View File

@ -98,7 +98,8 @@
name: "Pleroma",
email: "example@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5000,
limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
@ -137,8 +138,8 @@
logo_mask: true,
logo_margin: "0.1em",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/~/main/all",
redirect_root_login: "/~/main/friends",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
formatting_options_enabled: false,
@ -220,6 +221,37 @@
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
"about",
"~",
"main",
"users",
"settings",
"objects",
"activities",
"web",
"registration",
"friend-requests",
"pleroma",
"api",
"tag",
"notice",
"status",
"user-search",
"ostatus_subscribe",
"oauth",
"push",
"relay",
"inbox",
".well-known",
"nodeinfo",
"auth",
"proxy",
"dev",
"internal"
]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View File

@ -63,6 +63,7 @@ config :pleroma, Pleroma.Mailer,
* `email`: Email used to reach an Administrator/Moderator of the instance
* `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``
* `limit`: Posts character limit (CW/Subject included in the counter)
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner)
* `avatar_upload_limit`: File size limit of users profile avatars
* `background_upload_limit`: File size limit of users profile backgrounds

View File

@ -21,6 +21,8 @@ ProtectSystem=full
PrivateDevices=false
; Ensures that the service process and all its children can never gain new privileges through execve().
NoNewPrivileges=true
; Drops the sysadmin capability from the daemon.
CapabilityBoundingSet=~CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target

View File

@ -75,7 +75,7 @@ def run(["gen" | rest]) do
name =
Common.get_option(
options,
:name,
:instance_name,
"What is the name of your instance? (e.g. Pleroma/Soykaf)"
)

View File

@ -7,6 +7,8 @@ defmodule Pleroma.Activity do
alias Pleroma.{Repo, Activity, Notification}
import Ecto.Query
@type t :: %__MODULE__{}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
"Create" => "mention",

View File

@ -10,6 +10,8 @@ defmodule Pleroma.HTTP do
alias Pleroma.HTTP.Connection
alias Pleroma.HTTP.RequestBuilder, as: Builder
@type t :: __MODULE__
@doc """
Builds and perform http request.

View File

@ -80,9 +80,8 @@ def get(%{id: user_id} = _user, id) do
end
def clear(user) do
query = from(n in Notification, where: n.user_id == ^user.id)
Repo.delete_all(query)
from(n in Notification, where: n.user_id == ^user.id)
|> Repo.delete_all()
end
def dismiss(%{id: user_id} = _user, id) do

View File

@ -4,7 +4,8 @@
defmodule Pleroma.Object do
use Ecto.Schema
alias Pleroma.{Repo, Object, User, Activity, HTML}
alias Pleroma.{Repo, Object, User, Activity, HTML, ObjectTombstone}
alias Pleroma.{Repo, Object, User, Activity, ObjectTombstone}
import Ecto.{Query, Changeset}
schema "objects" do
@ -66,8 +67,25 @@ def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
formerType: type,
deleted: deleted
}
|> Map.from_struct()
end
def swap_object_with_tombstone(object) do
tombstone = make_tombstone(object)
object
|> Object.change(%{data: tombstone})
|> Repo.update()
end
def delete(%Object{data: %{"id" => id}} = object) do
with Repo.delete(object),
with {:ok, _obj} = swap_object_with_tombstone(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}

View File

@ -0,0 +1,4 @@
defmodule Pleroma.ObjectTombstone do
@enforce_keys [:id, :formerType, :deleted]
defstruct [:id, :formerType, :deleted, type: "Tombstone"]
end

View File

@ -13,6 +13,8 @@ defmodule Pleroma.User do
alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
require Logger
@type t :: %__MODULE__{}
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@ -47,6 +49,14 @@ def auth_active?(%User{} = user) do
!Pleroma.Config.get([:instance, :account_activation_required])
end
def remote_or_auth_active?(%User{} = user), do: !user.local || auth_active?(user)
def visible_for?(%User{} = user, for_user \\ nil) do
User.remote_or_auth_active?(user) || (for_user && for_user.id == user.id) ||
User.superuser?(for_user)
end
def superuser?(nil), do: false
def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info)
def avatar_url(user) do
@ -197,6 +207,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
@ -330,6 +341,24 @@ def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with %User{} = followed <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
def locked?(%User{} = user) do
user.info.locked || false
end
@ -366,7 +395,11 @@ def get_cached_by_nickname(nickname) do
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname)
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
[local_nickname, _] = String.split(nickname, "@")
Repo.get_by(User, nickname: local_nickname)
end
end
def get_by_nickname_or_email(nickname_or_email) do
@ -595,6 +628,23 @@ def search(query, resolve \\ false) do
Repo.all(q)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with %User{} = blocked <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
@ -648,6 +698,9 @@ def blocks?(user, %{ap_id: ap_id}) do
end)
end
def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
def block_domain(user, domain) do
info_cng =
user.info

View File

@ -56,10 +56,18 @@ defp check_actor_is_active(actor) do
end
end
defp check_remote_limit(%{"object" => %{"content" => content}}) do
limit = Pleroma.Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map),
:ok <- check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
:ok <- insert_full_object(map) do
{recipients, _, _} = get_recipients(map)
@ -503,6 +511,12 @@ defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
# Only search through last 100_000 activities by default
defp restrict_recent(query, %{"whole_db" => true}), do: query
@ -561,6 +575,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
end
def fetch_activities(recipients, opts \\ %{}) do

View File

@ -4,11 +4,12 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.{User, Object}
alias Pleroma.{Activity, User, Object}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
require Logger
@ -93,19 +94,15 @@ def followers(conn, %{"nickname" => nickname}) do
end
end
def outbox(conn, %{"nickname" => nickname, "max_id" => max_id}) do
def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: max_id}))
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
end
end
def outbox(conn, %{"nickname" => nickname}) do
outbox(conn, %{"nickname" => nickname, "max_id" => nil})
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- Utils.recipient_in_message(user.ap_id, params),
@ -156,6 +153,57 @@ def relay(conn, _params) do
end
end
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else
conn
|> put_status(:forbidden)
|> json("can't read inbox of #{nickname} as #{user.nickname}")
end
end
def update_outbox(
%{assigns: %{user: user}} = conn,
%{"nickname" => nickname, "type" => "Create"} = params
) do
if nickname == user.nickname do
actor = user.ap_id()
params =
params
|> Map.drop(["id"])
|> Map.put("actor", actor)
|> Transmogrifier.fix_addressing()
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", actor)
|> Transmogrifier.fix_object()
with {:ok, %Activity{} = activity} <-
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
}) do
conn
|> put_status(:created)
|> put_resp_header("location", activity.data["id"])
|> json(activity.data)
end
else
conn
|> put_status(:forbidden)
|> json("can't update outbox of #{nickname} as #{user.nickname}")
end
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)

View File

@ -176,6 +176,53 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
end
end
def render("inbox.json", %{user: user, max_id: max_qid}) do
params = %{
"limit" => "10"
}
params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id
collection =
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
iri = "#{user.ap_id}/inbox"
page = %{
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => -1,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id - 1}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"totalItems" => -1,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)

View File

@ -102,7 +102,7 @@ def post(user, %{"status" => status} = data) do
attachments,
tags,
get_content_type(data["content_type"]),
data["no_attachment_links"]
Enum.member?([true, "true"], data["no_attachment_links"])
),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],

View File

@ -704,11 +704,9 @@ def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
end
end
# TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info.blocks || [],
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
with blocked_accounts <- User.blocked_users(user) do
res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
json(conn, res)
end
end

View File

@ -11,10 +11,55 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.HTML
def render("accounts.json", %{users: users} = opts) do
render_many(users, AccountView, "account.json", opts)
users
|> render_many(AccountView, "account.json", opts)
|> Enum.filter(&Enum.any?/1)
end
def render("account.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]),
do: do_render("account.json", opts),
else: %{}
end
def render("mention.json", %{user: user}) do
%{
id: to_string(user.id),
acct: user.nickname,
username: username_from_nickname(user.nickname),
url: user.ap_id
}
end
def render("relationship.json", %{user: user, target: target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested =
if follow_activity do
follow_activity.data["state"] == "pending"
else
false
end
%{
id: to_string(target.id),
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks?(user, target),
muting: false,
muting_notifications: false,
requested: requested,
domain_blocking: false,
showing_reblogs: false,
endorsed: false
}
end
def render("relationships.json", %{user: user, targets: targets}) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end
defp do_render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
@ -72,43 +117,6 @@ def render("account.json", %{user: user} = opts) do
}
end
def render("mention.json", %{user: user}) do
%{
id: to_string(user.id),
acct: user.nickname,
username: username_from_nickname(user.nickname),
url: user.ap_id
}
end
def render("relationship.json", %{user: user, target: target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested =
if follow_activity do
follow_activity.data["state"] == "pending"
else
false
end
%{
id: to_string(target.id),
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks?(user, target),
muting: false,
muting_notifications: false,
requested: requested,
domain_blocking: false,
showing_reblogs: false,
endorsed: false
}
end
def render("relationships.json", %{user: user, targets: targets}) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end
defp username_from_nickname(string) when is_binary(string) do
hd(String.split(string, "@"))
end

View File

@ -138,7 +138,8 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
},
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
features: features
features: features,
restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
}
}

View File

@ -137,6 +137,7 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
@ -281,6 +282,7 @@ defmodule Pleroma.Web.Router do
get("/statuses/followers", TwitterAPI.Controller, :followers)
get("/statuses/friends", TwitterAPI.Controller, :friends)
get("/statuses/blocks", TwitterAPI.Controller, :blocks)
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
@ -410,6 +412,27 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
pipeline :activitypub_client do
plug(:accepts, ["activity+json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)

View File

@ -161,16 +161,21 @@ def remote_users(%{data: %{"to" => to} = data}) do
|> Enum.filter(fn user -> user && !user.local end)
end
defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do
# push an activity to remote accounts
#
defp send_to_user(%{info: %{salmon: salmon}}, feed, poster),
do: send_to_user(salmon, feed, poster)
defp send_to_user(url, feed, poster) when is_binary(url) do
with {:ok, %{status: code}} <-
poster.(
salmon,
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
) do
Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
else
e -> Logger.debug(fn -> "Pushing Salmon to #{salmon} failed, #{inspect(e)}" end)
e -> Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
end
end
@ -184,6 +189,11 @@ defp send_to_user(_, _, _), do: nil
"Undo",
"Delete"
]
@doc """
Publishes an activity to remote accounts
"""
@spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none
def publish(user, activity, poster \\ &@httpoison.post/3)
def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster)

View File

@ -240,22 +240,23 @@ def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
follow_import(conn, %{"list" => File.read!(listfile.path)})
end
def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
Task.start(fn ->
String.split(list)
|> Enum.map(fn account ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch(account),
{:ok, follower} <- User.maybe_direct_follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")
end
end)
end)
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
with followed_identifiers <- String.split(list),
{:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do
json(conn, "job started")
end
end
def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
blocks_import(conn, %{"list" => File.read!(listfile.path)})
end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
with blocked_identifiers <- String.split(list),
{:ok, _} = Task.start(fn -> User.blocks_import(blocker, blocked_identifiers) end) do
json(conn, "job started")
end
end
def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do

View File

@ -130,6 +130,15 @@ def show_user(conn, params) do
def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do
{:ok, target_user} ->
# Twitter and ActivityPub use a different name and sense for this parameter.
{include_rts, params} = Map.pop(params, "include_rts")
params =
case include_rts do
x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
_ -> params
end
activities = ActivityPub.fetch_user_activities(target_user, user, params)
conn
@ -498,6 +507,14 @@ def friends(%{assigns: %{user: for_user}} = conn, params) do
end
end
def blocks(%{assigns: %{user: user}} = conn, _params) do
with blocked_users <- User.blocked_users(user) do
conn
|> put_view(UserView)
|> render("index.json", %{users: blocked_users, for: user})
end
end
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
@ -653,7 +670,7 @@ defp forbidden_json_reply(conn, error_message) do
json_reply(conn, 403, json)
end
def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn
def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
def only_if_public_instance(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :public) do

View File

@ -15,18 +15,44 @@ def render("show.json", %{user: user = %User{}} = assigns) do
end
def render("index.json", %{users: users, for: user}) do
render_many(users, Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
users
|> render_many(Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
|> Enum.filter(&Enum.any?/1)
end
def render("user.json", %{user: user = %User{}} = assigns) do
if User.visible_for?(user, assigns[:for]),
do: do_render("user.json", assigns),
else: %{}
end
def render("short.json", %{
user: %User{
nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{
"fullname" => name,
"id" => id,
"ostatus_uri" => ap_id,
"profile_url" => ap_id,
"screen_name" => nickname
}
end
defp do_render("user.json", %{user: user = %User{}} = assigns) do
for_user = assigns[:for]
image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} =
if assigns[:for] do
if for_user do
{
User.following?(assigns[:for], user),
User.following?(user, assigns[:for]),
User.blocks?(assigns[:for], user)
User.following?(for_user, user),
User.following?(user, for_user),
User.blocks?(for_user, user)
}
else
{false, false, false}
@ -51,7 +77,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
"description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(assigns[:for])),
"description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
@ -97,23 +123,6 @@ def render("user.json", %{user: user = %User{}} = assigns) do
end
end
def render("short.json", %{
user: %User{
nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{
"fullname" => name,
"id" => id,
"ostatus_uri" => ap_id,
"profile_url" => ap_id,
"screen_name" => nickname
}
end
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil

View File

@ -5,7 +5,7 @@ def project do
[
app: :pleroma,
version: version("0.9.0"),
elixir: "~> 1.4",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: true],
@ -71,7 +71,7 @@ defp deps do
{:crypt,
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:cors_plug, "~> 1.5"},
{:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:web_push_encryption, "~> 0.2.1"},
{:swoosh, "~> 0.20"},
{:gen_smtp, "~> 0.13"},

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.285fa56c62b811bbd37880f7e2656b13.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.60e190da7cc4cc4711dc.js></script><script type=text/javascript src=/static/js/vendor.48d4753220bd83360796.js></script><script type=text/javascript src=/static/js/app.6cb7378f44092df9536a.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.44bcebbab7b3203648fdb538eb16129b.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.18ee0a4963e1e9ec7ea6.js></script><script type=text/javascript src=/static/js/vendor.21f9327c919db89265c3.js></script><script type=text/javascript src=/static/js/app.f5ecd4e55f996aad6b8a.js></script></body></html>

View File

@ -4,8 +4,8 @@
"logo": "/static/logo.png",
"logoMask": true,
"logoMargin": ".1em",
"redirectRootNoLogin": "/~/main/all",
"redirectRootLogin": "/~/main/friends",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"chatDisabled": false,
"showInstanceSpecificPanel": false,
"scopeOptionsEnabled": false,
@ -16,5 +16,7 @@
"alwaysShowSubjectInput": true,
"hidePostStats": false,
"hideUserStats": false,
"loginMethod": "password"
"loginMethod": "password",
"webPushNotifications": false,
"noAttachmentLinks": false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,18 +4,19 @@
defmodule Pleroma.ActivityTest do
use Pleroma.DataCase
alias Pleroma.Activity
import Pleroma.Factory
test "returns an activity by it's AP id" do
activity = insert(:note_activity)
found_activity = Pleroma.Activity.get_by_ap_id(activity.data["id"])
found_activity = Activity.get_by_ap_id(activity.data["id"])
assert activity == found_activity
end
test "returns activities by it's objects AP ids" do
activity = insert(:note_activity)
[found_activity] = Pleroma.Activity.all_by_object_ap_id(activity.data["object"]["id"])
[found_activity] = Activity.all_by_object_ap_id(activity.data["object"]["id"])
assert activity == found_activity
end
@ -23,8 +24,7 @@ test "returns activities by it's objects AP ids" do
test "returns the activity that created an object" do
activity = insert(:note_activity)
found_activity =
Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"])
found_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"])
assert activity == found_activity
end

View File

@ -0,0 +1,9 @@
{
"@context": ["https://www.w3.org/ns/activitystreams", {"@language": "en-GB"}],
"type": "Create",
"object": {
"type": "Note",
"content": "It's a note"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"]
}

View File

@ -36,6 +36,8 @@ test "deletes an object" do
found_object = Object.get_by_ap_id(object.data["id"])
refute object == found_object
assert found_object.data["type"] == "Tombstone"
end
test "ensures cache is cleared for the object" do
@ -51,6 +53,8 @@ test "ensures cache is cleared for the object" do
cached_object = Object.get_cached_by_ap_id(object.data["id"])
refute object == cached_object
assert cached_object.data["type"] == "Tombstone"
end
end
end

View File

@ -153,6 +153,20 @@ test "it requires an email, name, nickname and password, bio is optional" do
end)
end
test "it restricts certain nicknames" do
[restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
assert is_bitstring(restricted_name)
params =
@full_user_data
|> Map.put(:nickname, restricted_name)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
test "it sets the password_hash, ap_id and following fields" do
changeset = User.register_changeset(%User{}, @full_user_data)
@ -264,6 +278,24 @@ test "gets an existing user, case insensitive" do
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname" do
user = insert(:user)
fetched_user =
User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname, case insensitive" do
user = insert(:user, nickname: "nick")
casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
fetched_user = User.get_or_fetch_by_nickname(casing_altered_fqn)
assert user == fetched_user
end
test "fetches an external user via ostatus if no user exists" do
fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
assert fetched_user.nickname == "shp@social.heldscal.la"
@ -471,6 +503,21 @@ test "it sets the info->follower_count property" do
end
end
describe "follow_import" do
test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
result = User.follow_import(user1, identifiers)
assert is_list(result)
assert result == [user2, user3]
end
end
describe "blocks" do
test "it blocks people" do
user = insert(:user)
@ -570,6 +617,21 @@ test "unblocks domains" do
end
end
describe "blocks_import" do
test "it imports user blocks from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
result = User.blocks_import(user1, identifiers)
assert is_list(result)
assert result == [user2, user3]
end
end
test "get recipients from activity" do
actor = insert(:user)
user = insert(:user, local: true)

View File

@ -112,6 +112,32 @@ test "it inserts an incoming activity into the database", %{conn: conn} do
:timer.sleep(500)
assert Activity.get_by_ap_id(data["id"])
end
test "it rejects reads from other users", %{conn: conn} do
user = insert(:user)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert response(conn, 200) =~ note_activity.data["object"]["content"]
end
end
describe "/users/:nickname/outbox" do
@ -138,6 +164,34 @@ test "it returns an announce activity in a collection", %{conn: conn} do
assert response(conn, 200) =~ announce_activity.data["object"]
end
test "it rejects posts from other users", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
end
end
describe "/users/:nickname/followers" do

View File

@ -31,6 +31,24 @@ test "it returns a user" do
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
|> Base.encode64()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
@ -180,6 +198,16 @@ test "doesn't return blocked activities" do
assert Enum.member?(activities, activity_one)
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
@ -482,7 +510,7 @@ test "it creates a delete activity and deletes the original object" do
assert Repo.get(Activity, delete.id) != nil
assert Repo.get(Object, object.id) == nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
end

View File

@ -296,7 +296,7 @@ test "when you created it", %{conn: conn} do
assert %{} = json_response(conn, 200)
assert Repo.get(Activity, activity.id) == nil
refute Repo.get(Activity, activity.id)
end
test "when you didn't create it", %{conn: conn} do
@ -840,6 +840,26 @@ test "gets an users media", %{conn: conn} do
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
end
test "gets a user's statuses without reblogs", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, _, _} = CommonAPI.repeat(post.id, user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
end
end
describe "user relationships" do

View File

@ -19,6 +19,17 @@ test "nodeinfo shows staff accounts", %{conn: conn} do
assert user.ap_id in result["metadata"]["staffAccounts"]
end
test "nodeinfo shows restricted nicknames", %{conn: conn} do
conn =
conn
|> get("/nodeinfo/2.0.json")
assert result = json_response(conn, 200)
assert Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) ==
result["metadata"]["restrictedNicknames"]
end
test "returns 404 when federation is disabled", %{conn: conn} do
instance =
Application.get_env(:pleroma, :instance)

View File

@ -25,7 +25,7 @@ test "it removes the mentioned activity" do
refute Repo.get(Activity, note.id)
refute Repo.get(Activity, like.id)
refute Object.get_by_ap_id(note.data["object"]["id"])
assert Object.get_by_ap_id(note.data["object"]["id"]).data["type"] == "Tombstone"
assert Repo.get(Activity, second_note.id)
assert Object.get_by_ap_id(second_note.data["object"]["id"])

View File

@ -5,7 +5,7 @@
defmodule Pleroma.Web.OStatus.OStatusControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.{User, Repo}
alias Pleroma.{User, Repo, Object}
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus.ActivityRepresenter
@ -114,6 +114,22 @@ test "404s on nonexisting objects", %{conn: conn} do
|> response(404)
end
test "404s on deleted objects", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"]))
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
conn
|> get("/objects/#{uuid}")
|> response(200)
Object.delete(object)
conn
|> get("/objects/#{uuid}")
|> response(404)
end
test "gets an activity", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))

View File

@ -112,6 +112,8 @@ test "with credentials", %{conn: conn, user: user} do
end
describe "GET /statuses/public_timeline.json" do
setup [:valid_user]
test "returns statuses", %{conn: conn} do
user = insert(:user)
activities = ActivityBuilder.insert_list(30, %{}, %{user: user})
@ -145,14 +147,44 @@ test "returns 403 to unauthenticated request when the instance is not public", %
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to authenticated request when the instance is not public",
%{conn: conn, user: user} do
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, false)
Application.put_env(:pleroma, :instance, instance)
conn
|> with_credentials(user.nickname, "test")
|> get("/api/statuses/public_timeline.json")
|> json_response(200)
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, true)
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
conn
|> get("/api/statuses/public_timeline.json")
|> json_response(200)
end
test "returns 200 to authenticated request when the instance is public",
%{conn: conn, user: user} do
conn
|> with_credentials(user.nickname, "test")
|> get("/api/statuses/public_timeline.json")
|> json_response(200)
end
end
describe "GET /statuses/public_and_external_timeline.json" do
setup [:valid_user]
test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
instance =
Application.get_env(:pleroma, :instance)
@ -171,11 +203,39 @@ test "returns 403 to unauthenticated request when the instance is not public", %
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to authenticated request when the instance is not public",
%{conn: conn, user: user} do
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, false)
Application.put_env(:pleroma, :instance, instance)
conn
|> with_credentials(user.nickname, "test")
|> get("/api/statuses/public_and_external_timeline.json")
|> json_response(200)
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, true)
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
conn
|> get("/api/statuses/public_and_external_timeline.json")
|> json_response(200)
end
test "returns 200 to authenticated request when the instance is public",
%{conn: conn, user: user} do
conn
|> with_credentials(user.nickname, "test")
|> get("/api/statuses/public_and_external_timeline.json")
|> json_response(200)
end
end
describe "GET /statuses/show/:id.json" do
@ -519,6 +579,34 @@ test "with credentials screen_name", %{conn: conn, user: current_user} do
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with credentials with user_id, excluding RTs", %{conn: conn, user: current_user} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1, "type" => "Create"}, %{user: user})
{:ok, _} = ActivityBuilder.insert(%{"id" => 2, "type" => "Announce"}, %{user: user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json", %{
"user_id" => user.id,
"include_rts" => "false"
})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
conn =
conn
|> get("/api/statuses/user_timeline.json", %{"user_id" => user.id, "include_rts" => "0"})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
end
describe "POST /friendships/create.json" do
@ -1057,6 +1145,24 @@ test "it returns the followers for a hidden network if requested by the user the
end
end
describe "GET /api/statuses/blocks" do
test "it returns the list of users blocked by requester", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/blocks")
expected = UserView.render("index.json", %{users: [other_user], for: user})
result = json_response(conn, 200)
assert Enum.sort(expected) == Enum.sort(result)
end
end
describe "GET /api/statuses/friends" do
test "it returns the logged in user's friends", %{conn: conn} do
user = insert(:user)

View File

@ -0,0 +1,35 @@
defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
describe "POST /api/pleroma/follow_import" do
test "it returns HTTP 200", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
end
describe "POST /api/pleroma/blocks_import" do
test "it returns HTTP 200", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
end
end