Merge branch 'develop' into csaurus/pleroma-feature/mstdn-direct-api

This commit is contained in:
lain 2018-05-26 16:00:59 +02:00
commit 4b3ec53514
54 changed files with 1467 additions and 180 deletions

View file

@ -56,6 +56,8 @@
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true public: true
config :pleroma, :activitypub, accept_blocks: true
config :pleroma, :mrf_simple, config :pleroma, :mrf_simple,
media_removal: [], media_removal: [],
media_nsfw: [], media_nsfw: [],

View file

@ -21,7 +21,7 @@
pool: Ecto.Adapters.SQL.Sandbox pool: Ecto.Adapters.SQL.Sandbox
# Reduce hash rounds for testing # Reduce hash rounds for testing
config :comeonin, :pbkdf2_rounds, 1 config :pbkdf2_elixir, rounds: 1
config :pleroma, :websub, Pleroma.Web.WebsubMock config :pleroma, :websub, Pleroma.Web.WebsubMock
config :pleroma, :ostatus, Pleroma.Web.OStatusMock config :pleroma, :ostatus, Pleroma.Web.OStatusMock

View file

@ -52,8 +52,9 @@ server {
# if you do not want remote frontends to be able to access your Pleroma backend # if you do not want remote frontends to be able to access your Pleroma backend
# server, remove these lines. # server, remove these lines.
add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS' always; add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
if ($request_method = OPTIONS) { if ($request_method = OPTIONS) {
return 204; return 204;
} }

View file

@ -5,6 +5,7 @@ defmodule Pleroma.Application do
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
import Supervisor.Spec import Supervisor.Spec
import Cachex.Spec
# Define workers and child supervisors to be supervised # Define workers and child supervisors to be supervised
children = children =
@ -28,8 +29,11 @@ def start(_type, _args) do
[ [
:idempotency_cache, :idempotency_cache,
[ [
default_ttl: :timer.seconds(6 * 60 * 60), expiration:
ttl_interval: :timer.seconds(60), expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500 limit: 2500
] ]
], ],

87
lib/pleroma/list.ex Normal file
View file

@ -0,0 +1,87 @@
defmodule Pleroma.List do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{User, Repo}
schema "lists" do
belongs_to(:user, Pleroma.User)
field(:title, :string)
field(:following, {:array, :string}, default: [])
timestamps()
end
def title_changeset(list, attrs \\ %{}) do
list
|> cast(attrs, [:title])
|> validate_required([:title])
end
def follow_changeset(list, attrs \\ %{}) do
list
|> cast(attrs, [:following])
|> validate_required([:following])
end
def for_user(user, opts) do
query =
from(
l in Pleroma.List,
where: l.user_id == ^user.id,
order_by: [desc: l.id],
limit: 50
)
Repo.all(query)
end
def get(id, %{id: user_id} = _user) do
query =
from(
l in Pleroma.List,
where: l.id == ^id,
where: l.user_id == ^user_id
)
Repo.one(query)
end
def get_following(%Pleroma.List{following: following} = list) do
q =
from(
u in User,
where: u.follower_address in ^following
)
{:ok, Repo.all(q)}
end
def rename(%Pleroma.List{} = list, title) do
list
|> title_changeset(%{title: title})
|> Repo.update()
end
def create(title, %User{} = creator) do
list = %Pleroma.List{user_id: creator.id, title: title}
Repo.insert(list)
end
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
update_follows(list, %{following: Enum.uniq([followed.follower_address | following])})
end
def unfollow(%Pleroma.List{following: following} = list, %User{} = unfollowed) do
update_follows(list, %{following: List.delete(following, unfollowed.follower_address)})
end
def delete(%Pleroma.List{} = list) do
Repo.delete(list)
end
def update_follows(%Pleroma.List{} = list, attrs) do
list
|> follow_changeset(attrs)
|> Repo.update()
end
end

View file

@ -33,10 +33,7 @@ def get_cached_by_ap_id(ap_id) do
else else
key = "object:#{ap_id}" key = "object:#{ap_id}"
Cachex.get!( Cachex.fetch!(:user_cache, key, fn _ ->
:user_cache,
key,
fallback: fn _ ->
object = get_by_ap_id(ap_id) object = get_by_ap_id(ap_id)
if object do if object do
@ -44,8 +41,7 @@ def get_cached_by_ap_id(ap_id) do
else else
{:ignore, object} {:ignore, object}
end end
end end)
)
end end
end end

View file

@ -13,7 +13,7 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
end end
def call(conn, _opts) do def call(conn, _opts) do
user = Utils.normalize_actor(conn.params["actor"]) user = Utils.get_ap_id(conn.params["actor"])
Logger.debug("Checking sig for #{user}") Logger.debug("Checking sig for #{user}")
[signature | _] = get_req_header(conn, "signature") [signature | _] = get_req_header(conn, "signature")

View file

@ -67,7 +67,8 @@ def user_info(%User{} = user) do
%{ %{
following_count: length(user.following) - oneself, following_count: length(user.following) - oneself,
note_count: user.info["note_count"] || 0, note_count: user.info["note_count"] || 0,
follower_count: user.info["follower_count"] || 0 follower_count: user.info["follower_count"] || 0,
locked: user.info["locked"] || false
} }
end end
@ -167,12 +168,46 @@ def register_changeset(struct, params \\ %{}) do
end end
end end
def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
user_info = user_info(followed)
should_direct_follow =
cond do
# if the account is locked, don't pre-create the relationship
user_info["locked"] == true ->
false
# if the users are blocking each other, we shouldn't even be here, but check for it anyway
User.blocks?(follower, followed) == true or User.blocks?(followed, follower) == true ->
false
# if OStatus, then there is no three-way handshake to follow
User.ap_enabled?(followed) != true ->
true
# if there are no other reasons not to, just pre-create the relationship
true ->
true
end
if should_direct_follow do
follow(follower, followed)
else
follower
end
end
def follow(%User{} = follower, %User{info: info} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) or info["deactivated"] do cond do
following?(follower, followed) or info["deactivated"] ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."} {:error, "Could not follow user: #{followed.nickname} is already on your list."}
else
blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
if !followed.local && follower.local && !ap_enabled?(followed) do if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed) Websub.subscribe(follower, followed)
end end
@ -223,9 +258,9 @@ def get_by_ap_id(ap_id) do
def update_and_set_cache(changeset) do def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do with {:ok, user} <- Repo.update(changeset) do
Cachex.set(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.set(:user_cache, "nickname:#{user.nickname}", user) Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.set(:user_cache, "user_info:#{user.id}", user_info(user)) Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user} {:ok, user}
else else
e -> e e -> e
@ -239,12 +274,12 @@ def invalidate_cache(user) do
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn _ -> get_by_ap_id(ap_id) end) Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end end
def get_cached_by_nickname(nickname) do def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}" key = "nickname:#{nickname}"
Cachex.get!(:user_cache, key, fallback: fn _ -> get_or_fetch_by_nickname(nickname) end) Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
end end
def get_by_nickname(nickname) do def get_by_nickname(nickname) do
@ -260,7 +295,7 @@ def get_by_nickname_or_email(nickname_or_email) do
def get_cached_user_info(user) do def get_cached_user_info(user) do
key = "user_info:#{user.id}" key = "user_info:#{user.id}"
Cachex.get!(:user_cache, key, fallback: fn _ -> user_info(user) end) Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
end end
def fetch_by_nickname(nickname) do def fetch_by_nickname(nickname) do

View file

@ -104,6 +104,17 @@ def accept(%{to: to, actor: actor, object: object} = params) do
end end
end end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
@ -201,12 +212,11 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
end end
end end
def unfollow(follower, followed, local \\ true) do def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local),
:ok, :ok <- maybe_federate(activity) do
maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
end end
@ -230,6 +240,29 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
end end
end end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def fetch_activities_for_context(context, opts \\ %{}) do def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"] public = ["https://www.w3.org/ns/activitystreams#Public"]
@ -476,6 +509,7 @@ def user_data_from_user_object(data) do
"url" => [%{"href" => data["image"]["url"]}] "url" => [%{"href" => data["image"]["url"]}]
} }
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{ user_data = %{
@ -483,7 +517,8 @@ def user_data_from_user_object(data) do
info: %{ info: %{
"ap_enabled" => true, "ap_enabled" => true,
"source_data" => data, "source_data" => data,
"banner" => banner "banner" => banner,
"locked" => locked
}, },
avatar: avatar, avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}", nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query import Ecto.Query
@ -145,6 +146,78 @@ def handle_incoming(
end end
end end
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
{:ok, activity}
else
_ -> {:error, nil}
end
end
defp mastodon_follow_hack(_), do: {:error, nil}
defp get_follow_activity(follow_object, followed) do
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
{:ok, activity}
else
# Can't find the activity. This might a Mastodon 2.3 "Accept"
{:activity, nil} ->
mastodon_follow_hack(follow_object, followed)
_ ->
{:error, nil}
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
if not User.following?(follower, followed) do
{:ok, follower} = User.follow(follower, followed)
end
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data
) do ) do
@ -207,11 +280,7 @@ def handle_incoming(
def handle_incoming( def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
) do ) do
object_id = object_id = Utils.get_ap_id(object_id)
case object_id do
%{"id" => id} -> id
id -> id
end
with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- {:ok, object} <-
@ -229,13 +298,68 @@ def handle_incoming(
"object" => %{"type" => "Announce", "object" => object_id}, "object" => %{"type" => "Announce", "object" => object_id},
"actor" => actor, "actor" => actor,
"id" => id "id" => id
} = data } = _data
) do ) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- {:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity} {:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
e -> :error
end
end
@ap_config Application.get_env(:pleroma, :activitypub)
@accept_blocks Keyword.get(@ap_config, :accept_blocks)
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data
) do
with true <- @accept_blocks,
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do
with true <- @accept_blocks,
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else else
e -> :error e -> :error
end end
@ -247,7 +371,7 @@ def handle_incoming(
"object" => %{"type" => "Like", "object" => object_id}, "object" => %{"type" => "Like", "object" => object_id},
"actor" => actor, "actor" => actor,
"id" => id "id" => id
} = data } = _data
) do ) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- {:ok, object} <-
@ -255,14 +379,10 @@ def handle_incoming(
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity} {:ok, activity}
else else
e -> :error _e -> :error
end end
end end
# TODO
# Accept
# Undo for non-Announce
def handle_incoming(_), do: :error def handle_incoming(_), do: :error
def get_obj_helper(id) do def get_obj_helper(id) do
@ -516,11 +636,11 @@ def maybe_retire_websub(ap_id) do
def maybe_fix_user_url(data) do def maybe_fix_user_url(data) do
if is_map(data["url"]) do if is_map(data["url"]) do
data = Map.put(data, "url", data["url"]["href"]) Map.put(data, "url", data["url"]["href"])
end else
data data
end end
end
def maybe_fix_user_object(data) do def maybe_fix_user_object(data) do
data data

View file

@ -7,18 +7,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
def normalize_actor(actor) do def get_ap_id(object) do
cond do case object do
is_binary(actor) -> %{"id" => id} -> id
actor id -> id
is_map(actor) ->
actor["id"]
end end
end end
def normalize_params(params) do def normalize_params(params) do
Map.put(params, "actor", normalize_actor(params["actor"])) Map.put(params, "actor", get_ap_id(params["actor"]))
end end
def make_json_ld_header do def make_json_ld_header do
@ -238,11 +235,17 @@ def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query = query =
from( from(
activity in Activity, activity in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
activity.data
),
where: activity.actor == ^follower_id,
where: where:
fragment( fragment(
"? @> ?", "? @> ?",
activity.data, activity.data,
^%{type: "Follow", actor: follower_id, object: followed_id} ^%{object: followed_id}
), ),
order_by: [desc: :id], order_by: [desc: :id],
limit: 1 limit: 1
@ -260,7 +263,7 @@ def get_existing_announce(actor, %{data: %{"id" => id}}) do
query = query =
from( from(
activity in Activity, activity in Activity,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor), where: activity.actor == ^actor,
# this is to use the index # this is to use the index
where: where:
fragment( fragment(
@ -346,13 +349,61 @@ def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
#### Unfollow-related helpers #### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity) do def make_unfollow_data(follower, followed, follow_activity, activity_id) do
%{ data = %{
"type" => "Undo", "type" => "Undo",
"actor" => follower.ap_id, "actor" => follower.ap_id,
"to" => [followed.ap_id], "to" => [followed.ap_id],
"object" => follow_activity.data["id"] "object" => follow_activity.data
} }
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
#### Block-related helpers
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Block'",
activity.data
),
where: activity.actor == ^blocker_id,
where:
fragment(
"? @> ?",
activity.data,
^%{object: blocked_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
def make_block_data(blocker, blocked, activity_id) do
data = %{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
data = %{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
#### Create-related helpers #### Create-related helpers

View file

@ -26,7 +26,7 @@ def render("user.json", %{user: user}) do
"name" => user.name, "name" => user.name,
"summary" => user.bio, "summary" => user.bio,
"url" => user.ap_id, "url" => user.ap_id,
"manuallyApprovesFollowers" => false, "manuallyApprovesFollowers" => user.info["locked"] || false,
"publicKey" => %{ "publicKey" => %{
"id" => "#{user.ap_id}#main-key", "id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id, "owner" => user.ap_id,

View file

@ -133,7 +133,7 @@ def make_note_data(
"context" => context, "context" => context,
"attachment" => attachments, "attachment" => attachments,
"actor" => actor, "actor" => actor,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
} }
if inReplyTo do if inReplyTo do
@ -187,9 +187,9 @@ defp shortname(name) do
end end
end end
def confirm_current_password(user, params) do def confirm_current_password(user, password) do
with %User{local: true} = db_user <- Repo.get(User, user.id), with %User{local: true} = db_user <- Repo.get(User, user.id),
true <- Pbkdf2.checkpw(params["password"], db_user.password_hash) do true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user} {:ok, db_user}
else else
_ -> {:error, "Invalid password."} _ -> {:error, "Invalid password."}

View file

@ -32,14 +32,14 @@ def validate(headers, signature, public_key) do
def validate_conn(conn) do def validate_conn(conn) do
# TODO: How to get the right key and see if it is actually valid for that request. # TODO: How to get the right key and see if it is actually valid for that request.
# For now, fetch the key for the actor. # For now, fetch the key for the actor.
with actor_id <- Utils.normalize_actor(conn.params["actor"]), with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
if validate_conn(conn, public_key) do if validate_conn(conn, public_key) do
true true
else else
Logger.debug("Could not validate, re-fetching user and trying one more time") Logger.debug("Could not validate, re-fetching user and trying one more time")
# Fetch user anew and try one more time # Fetch user anew and try one more time
with actor_id <- Utils.normalize_actor(conn.params["actor"]), with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
validate_conn(conn, public_key) validate_conn(conn, public_key)

View file

@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification, Stats} alias Pleroma.{Repo, Activity, User, Notification, Stats}
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{CommonAPI, OStatus} alias Pleroma.Web.{CommonAPI, OStatus}
alias Pleroma.Web.OAuth.{Authorization, Token, App} alias Pleroma.Web.OAuth.{Authorization, Token, App}
@ -284,11 +284,7 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
end end
{:ok, activity} = {:ok, activity} =
Cachex.get!( Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
:idempotency_cache,
idempotency_key,
fallback: fn _ -> CommonAPI.post(user, params) end
)
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}) render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end end
@ -442,7 +438,7 @@ def following(conn, %{"id" => id}) do
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do {:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed}) render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else else
@ -455,7 +451,7 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri), with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do {:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "account.json", %{user: followed}) render(conn, AccountView, "account.json", %{user: followed})
else else
@ -466,24 +462,18 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
end end
end end
# TODO: Clean up and unify
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower, follow_activity} <- User.unfollow(follower, followed), {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
{:ok, _activity} <- {:ok, follower, _} <- User.unfollow(follower, followed) do
ActivityPub.insert(%{
"type" => "Undo",
"actor" => follower.ap_id,
# get latest Follow for these users
"object" => follow_activity.data["id"]
}) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed}) render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
end end
end end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id), with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked) do {:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked}) render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else else
{:error, message} -> {:error, message} ->
@ -495,7 +485,8 @@ def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id), with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked) do {:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked}) render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else else
{:error, message} -> {:error, message} ->
@ -586,6 +577,102 @@ def favourites(%{assigns: %{user: user}} = conn, _) do
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e -> json(conn, "error")
end
end
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
else
_e ->
json(conn, "error")
end
end
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
end
end
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(User, account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(Pleroma.User, account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
end
end
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, list} <- Pleroma.List.rename(list, title) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e ->
json(conn, "error")
end
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
# adding title is a hack to not make empty lists function like a public timeline
activities =
ActivityPub.fetch_activities([title | following], params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Error."})
end
end
def index(%{assigns: %{user: user}} = conn, _params) do def index(%{assigns: %{user: user}} = conn, _params) do
token = token =
conn conn

View file

@ -19,7 +19,7 @@ def render("account.json", %{user: user}) do
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
acct: user.nickname, acct: user.nickname,
display_name: user.name || user.nickname, display_name: user.name || user.nickname,
locked: false, locked: user_info.locked,
created_at: Utils.to_masto_date(user.inserted_at), created_at: Utils.to_masto_date(user.inserted_at),
followers_count: user_info.follower_count, followers_count: user_info.follower_count,
following_count: user_info.following_count, following_count: user_info.following_count,

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Web.MastodonAPI.ListView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.ListView
def render("lists.json", %{lists: lists} = opts) do
render_many(lists, ListView, "list.json", opts)
end
def render("list.json", %{list: list}) do
%{
id: to_string(list.id),
title: list.title
}
end
end

View file

@ -232,7 +232,12 @@ def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author)
end end
# Only undos of follow for now. Will need to get redone once there are more # Only undos of follow for now. Will need to get redone once there are more
def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) do def to_simple_form(
%{data: %{"type" => "Undo", "object" => %{"type" => "Follow"} = follow_activity}} =
activity,
user,
with_author
) do
h = fn str -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
@ -240,16 +245,9 @@ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) d
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
follow_activity =
if is_map(activity.data["object"]) do
Activity.get_by_ap_id(activity.data["object"]["id"])
else
Activity.get_by_ap_id(activity.data["object"])
end
mentions = (activity.recipients || []) |> get_mentions mentions = (activity.recipients || []) |> get_mentions
follow_activity = Activity.get_by_ap_id(follow_activity["id"])
if follow_activity do
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
@ -268,7 +266,6 @@ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) d
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []} {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author ] ++ mentions ++ author
end end
end
def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end

View file

@ -0,0 +1,17 @@
defmodule Pleroma.Web.OStatus.UnfollowHandler do
alias Pleroma.Web.{XML, OStatus}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
{:ok, activity} <- ActivityPub.unfollow(actor, followed, id, false) do
User.unfollow(actor, followed)
{:ok, activity}
end
end
end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.{Repo, User, Web, Object, Activity} alias Pleroma.{Repo, User, Web, Object, Activity}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler} alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
def feed_path(user) do def feed_path(user) do
@ -47,6 +47,9 @@ def handle_incoming(xml_string) do
'http://activitystrea.ms/schema/1.0/follow' -> 'http://activitystrea.ms/schema/1.0/follow' ->
with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/unfollow' ->
with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/share' -> 'http://activitystrea.ms/schema/1.0/share' ->
with {:ok, activity, retweeted_activity} <- handle_share(entry, doc), with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
do: [activity, retweeted_activity] do: [activity, retweeted_activity]

View file

@ -73,6 +73,7 @@ def user_fetcher(username) do
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
post("/follow_import", UtilController, :follow_import) post("/follow_import", UtilController, :follow_import)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account) post("/delete_account", UtilController, :delete_account)
end end
@ -103,7 +104,6 @@ def user_fetcher(username) do
get("/domain_blocks", MastodonAPIController, :empty_array) get("/domain_blocks", MastodonAPIController, :empty_array)
get("/follow_requests", MastodonAPIController, :empty_array) get("/follow_requests", MastodonAPIController, :empty_array)
get("/mutes", MastodonAPIController, :empty_array) get("/mutes", MastodonAPIController, :empty_array)
get("/lists", MastodonAPIController, :empty_array)
get("/timelines/home", MastodonAPIController, :home_timeline) get("/timelines/home", MastodonAPIController, :home_timeline)
@ -125,6 +125,15 @@ def user_fetcher(username) do
get("/notifications/:id", MastodonAPIController, :get_notification) get("/notifications/:id", MastodonAPIController, :get_notification)
post("/media", MastodonAPIController, :upload) post("/media", MastodonAPIController, :upload)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
delete("/lists/:id", MastodonAPIController, :delete_list)
post("/lists", MastodonAPIController, :create_list)
put("/lists/:id", MastodonAPIController, :rename_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
end end
scope "/api/web", Pleroma.Web.MastodonAPI do scope "/api/web", Pleroma.Web.MastodonAPI do
@ -142,6 +151,7 @@ def user_fetcher(username) do
get("/timelines/public", MastodonAPIController, :public_timeline) get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context) get("/statuses/:id/context", MastodonAPIController, :get_context)

View file

@ -197,8 +197,31 @@ def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
json(conn, "job started") 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
{:ok, user} ->
with {:ok, _user} <-
User.reset_password(user, %{
password: params["new_password"],
password_confirmation: params["new_password_confirmation"]
}) do
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def delete_account(%{assigns: %{user: user}} = conn, params) do def delete_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params) do case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} -> {:ok, user} ->
Task.start(fn -> User.delete(user) end) Task.start(fn -> User.delete(user) end)
json(conn, %{status: "success"}) json(conn, %{status: "success"})

View file

@ -99,7 +99,7 @@ def to_map(
) do ) do
created_at = created_at |> Utils.date_to_asctime() created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} undid the action at #{undid_activity}" text = "#{user.nickname} undid the action at #{undid_activity["id"]}"
%{ %{
"id" => activity.id, "id" => activity.id,

View file

@ -25,7 +25,7 @@ def delete(%User{} = user, id) do
def follow(%User{} = follower, params) do def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params), with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed) do {:ok, activity} <- ActivityPub.follow(follower, followed) do
{:ok, follower, followed, activity} {:ok, follower, followed, activity}
else else
@ -36,14 +36,7 @@ def follow(%User{} = follower, params) do
def unfollow(%User{} = follower, params) do def unfollow(%User{} = follower, params) do
with {:ok, %User{} = unfollowed} <- get_user(params), with {:ok, %User{} = unfollowed} <- get_user(params),
{:ok, follower, follow_activity} <- User.unfollow(follower, unfollowed), {:ok, follower, follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
ActivityPub.insert(%{
"type" => "Undo",
"actor" => follower.ap_id,
# get latest Follow for these users
"object" => follow_activity.data["id"],
"published" => make_date()
}) do
{:ok, follower, unfollowed} {:ok, follower, unfollowed}
else else
err -> err err -> err
@ -52,7 +45,8 @@ def unfollow(%User{} = follower, params) do
def block(%User{} = blocker, params) do def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params), with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked) do {:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
{:ok, blocker, blocked} {:ok, blocker, blocked}
else else
err -> err err -> err
@ -61,7 +55,8 @@ def block(%User{} = blocker, params) do
def unblock(%User{} = blocker, params) do def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params), with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked) do {:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
{:ok, blocker, blocked} {:ok, blocker, blocked}
else else
err -> err err -> err

View file

@ -36,12 +36,13 @@ defp deps do
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.0.0"},
{:gettext, "~> 0.11"}, {:gettext, "~> 0.11"},
{:cowboy, "~> 1.0", override: true}, {:cowboy, "~> 1.0", override: true},
{:comeonin, "~> 3.0"}, {:comeonin, "~> 4.0"},
{:pbkdf2_elixir, "~> 0.12"},
{:trailing_format_plug, "~> 0.0.5"}, {:trailing_format_plug, "~> 0.0.5"},
{:html_sanitize_ex, "~> 1.3.0-rc1"}, {:html_sanitize_ex, "~> 1.3.0-rc1"},
{:phoenix_html, "~> 2.10"}, {:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.16.1"}, {:calendar, "~> 0.16.1"},
{:cachex, "~> 2.1"}, {:cachex, "~> 3.0"},
{:httpoison, "~> 1.1.0"}, {:httpoison, "~> 1.1.0"},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:ex_machina, "~> 2.0", only: :test}, {:ex_machina, "~> 2.0", only: :test},

View file

@ -1,42 +1,37 @@
%{ %{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "2.1.0", "fad49b4e78d11c6c314e75bd8c9408f5b78cb065c047442798caed10803ee3be", [:mix], [{:eternal, "~> 1.1", [hex: :eternal, repo: "hexpm", optional: false]}], "hexpm"}, "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.16.1", "782327ad8bae7c797b887840dc4ddb933f05ce6e333e5b04964d7a5d5f79bde3", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.16.1", "782327ad8bae7c797b887840dc4ddb933f05ce6e333e5b04964d7a5d5f79bde3", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"comeonin": {:hex, :comeonin, "3.2.0", "cb10995a22aed6812667efb3856f548818c85d85394d8132bc116fbd6995c1ef", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"con_cache": {:hex, :con_cache, "0.12.0", "2d961aec219aa5a914473873f348f5a6088292dc69d5192a9d25f8a1e13e9905", [:mix], [{:exactor, "~> 2.2.0", [hex: :exactor, optional: false]}]},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"credo": {:hex, :credo, "0.9.1", "f021affa11b32a94dc2e807a6472ce0914289c9132f99644a97fc84432b202a1", [: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.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"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, "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"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"deppie": {:hex, :deppie, "1.1.0", "cfb6fcee7dfb64eb78cb8505537971a0805131899326ad469ef10df04520f451", [:mix], []},
"ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [: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, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [: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"},
"elixir_make": {:hex, :elixir_make, "0.4.1", "6628b86053190a80b9072382bb9756a6c78624f208ec0ff22cb94c8977d80060", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"exactor": {:hex, :exactor, "2.2.3", "a6972f43bb6160afeb73e1d8ab45ba604cd0ac8b5244c557093f6e92ce582786", [:mix], []},
"fs": {:hex, :fs, "2.12.0", "ad631efacc9a5683c8eaa1b274e24fa64a1b8eb30747e9595b93bec7e492e25e", [:rebar3], []},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.1.0", "497949fb62924432f64a45269d20e6f61ecf35084ffa270917afcdb7cd4d8061", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.3.3", "70859889a8d1d43d1b75d69d87258a301f43209a17787cdb2bd9cab42adf271d", [:mix], [{:fs, "~> 2.12", [hex: :fs, optional: false]}]},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "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, "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_html": {:hex, :phoenix_html, "2.11.1", "77b6f7fbd252168c6ec4f573de648d37cc5258cda13266ef001fbf99267eb6f3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.11.2", "86ebd768258ba60a27f5578bec83095bdb93485d646fc4111db8844c316602d6", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "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.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"},
@ -45,4 +40,5 @@
"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"},
"tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
} }

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.CreateLists do
use Ecto.Migration
def change do
create table(:lists) do
add :user_id, references(:users, on_delete: :delete_all)
add :title, :string
add :following, {:array, :string}
timestamps()
end
create index(:lists, [:user_id])
end
end

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><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.c0e1e1e1fcff94fd1e14fc44bfee9a1e.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.f40706e221610d4c6256.js></script><script type=text/javascript src=/static/js/vendor.56aa9f8c34786f6af6b7.js></script><script type=text/javascript src=/static/js/app.029b23b3921537271958.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><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.c0e1e1e1fcff94fd1e14fc44bfee9a1e.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.16ab7851cdbf730f9cbc.js></script><script type=text/javascript src=/static/js/vendor.56aa9f8c34786f6af6b7.js></script><script type=text/javascript src=/static/js/app.13c0bda10eb515cdf8ed.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
!function(e){function t(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var p,l,s=0,i=[];s<c.length;s++)l=c[s],n[l]&&i.push.apply(i,n[l]),n[l]=0;for(p in o)Object.prototype.hasOwnProperty.call(o,p)&&(e[p]=o[p]);for(a&&a(c,o);i.length;)i.shift().call(null,t);if(o[0])return r[0]=0,t(0)};var r={},n={0:0};t.e=function(e,a){if(0===n[e])return a.call(null,t);if(void 0!==n[e])n[e].push(a);else{n[e]=[a];var r=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"56aa9f8c34786f6af6b7",2:"13c0bda10eb515cdf8ed"}[e]+".js",r.appendChild(c)}},t.m=e,t.c=r,t.p="/"}([]);
//# sourceMappingURL=manifest.16ab7851cdbf730f9cbc.js.map

View file

@ -1,2 +0,0 @@
!function(e){function t(r){if(a[r])return a[r].exports;var n=a[r]={exports:{},id:r,loaded:!1};return e[r].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var r=window.webpackJsonp;window.webpackJsonp=function(o,p){for(var c,l,s=0,i=[];s<o.length;s++)l=o[s],n[l]&&i.push.apply(i,n[l]),n[l]=0;for(c in p)Object.prototype.hasOwnProperty.call(p,c)&&(e[c]=p[c]);for(r&&r(o,p);i.length;)i.shift().call(null,t);if(p[0])return a[0]=0,t(0)};var a={},n={0:0};t.e=function(e,r){if(0===n[e])return r.call(null,t);if(void 0!==n[e])n[e].push(r);else{n[e]=[r];var a=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"56aa9f8c34786f6af6b7",2:"029b23b3921537271958"}[e]+".js",a.appendChild(o)}},t.m=e,t.c=a,t.p="/"}([]);
//# sourceMappingURL=manifest.f40706e221610d4c6256.js.map

View file

@ -0,0 +1,29 @@
{
"type": "Block",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T13:29:31Z"
},
"object": "http://localtesting.pleroma.lol/users/lain",
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#follows/2",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View file

@ -0,0 +1,34 @@
{
"type": "Reject",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "rBzK4Kqhd4g7HDS8WE5oRbWQb2R+HF/6awbUuMWhgru/xCODT0SJWSri0qWqEO4fPcpoUyz2d25cw6o+iy9wiozQb3hQNnu69AR+H5Mytc06+g10KCHexbGhbAEAw/7IzmeXELHUbaqeduaDIbdt1zw4RkwLXdqgQcGXTJ6ND1wM3WMHXQCK1m0flasIXFoBxpliPAGiElV8s0+Ltuh562GvflG3kB3WO+j+NaR0ZfG5G9N88xMj9UQlCKit5gpAE5p6syUsCU2WGBHywTumv73i3OVTIFfq+P9AdMsRuzw1r7zoKEsthW4aOzLQDi01ZjvdBz8zH6JnjDU7SMN/Ig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T14:36:41Z"
},
"object": {
"type": "Follow",
"object": "http://mastodon.example.org/users/admin",
"id": "http://localtesting.pleroma.lol/users/lain#follows/4",
"actor": "http://localtesting.pleroma.lol/users/lain"
},
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#rejects/follows/4",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View file

@ -0,0 +1,34 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
],
"signature": {
"type": "RsaSignature2017",
"signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T13:29:31Z"
},
"type": "Undo",
"object": {
"type": "Block",
"object": "http://localtesting.pleroma.lol/users/lain",
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#blocks/2",
"actor": "http://mastodon.example.org/users/admin"
},
"actor": "http://mastodon.example.org/users/admin",
"id": "http://mastodon.example.org/users/admin#blocks/2/undo"
}

View file

@ -0,0 +1,34 @@
{
"@context":[
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot":"http://joinmastodon.org/ns#",
"sensitive":"as:sensitive",
"ostatus":"http://ostatus.org#",
"movedTo":"as:movedTo",
"manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
"inReplyToAtomUri":"ostatus:inReplyToAtomUri",
"conversation":"ostatus:conversation",
"atomUri":"ostatus:atomUri",
"Hashtag":"as:Hashtag",
"Emoji":"toot:Emoji"
}
],
"signature":{
"type":"RsaSignature2017",
"signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator":"http://mastodon.example.org/users/admin#main-key",
"created":"2018-02-17T13:29:31Z"
},
"type":"Undo",
"object":{
"type":"Follow",
"object":"http://localtesting.pleroma.lol/users/lain",
"nickname":"lain",
"id":"http://mastodon.example.org/users/admin#follows/2",
"actor":"http://mastodon.example.org/users/admin"
},
"actor":"http://mastodon.example.org/users/admin",
"id": "http://mastodon.example.org/users/admin#follow/2/undo"
}

68
test/fixtures/unfollow.xml vendored Normal file
View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
<generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
<id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
<title>lambadalambda timeline</title>
<subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
<logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
<updated>2017-05-07T09:54:49+00:00</updated>
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://social.heldscal.la/user/23211</uri>
<name>lambadalambda</name>
<summary>Call me Deacon Blues.</summary>
<link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
<link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
<link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
<link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
<link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
<poco:preferredUsername>lambadalambda</poco:preferredUsername>
<poco:displayName>Constance Variable</poco:displayName>
<poco:note>Call me Deacon Blues.</poco:note>
<poco:address>
<poco:formatted>Berlin</poco:formatted>
</poco:address>
<poco:urls>
<poco:type>homepage</poco:type>
<poco:value>https://heldscal.la</poco:value>
<poco:primary>true</poco:primary>
</poco:urls>
<followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
<statusnet:profile_info local_id="23211"></statusnet:profile_info>
</author>
<link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
<link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
<link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
<link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
<link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
<link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
<link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
<entry>
<id>undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00</id>
<title>Constance Variable (lambadalambda@social.heldscal.la)'s status on Sunday, 07-May-2017 09:54:49 UTC</title>
<content type="html">&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot;&gt;Constance Variable&lt;/a&gt; stopped following &lt;a href=&quot;https://pawoo.net/@pekorino&quot;&gt;&lt;/a&gt;.</content>
<link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2092981"/>
<activity:verb>http://activitystrea.ms/schema/1.0/unfollow</activity:verb>
<published>2017-05-07T09:54:49+00:00</published>
<updated>2017-05-07T09:54:49+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id>https://pawoo.net/users/pekorino</id>
<title></title>
<summary>http://shitposter.club/mono 孤独のグルメ</summary>
<link rel="alternate" type="text/html" href="https://pawoo.net/@pekorino"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://social.heldscal.la/theme/neo-gnu/default-avatar-profile.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://social.heldscal.la/theme/neo-gnu/default-avatar-stream.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://social.heldscal.la/theme/neo-gnu/default-avatar-mini.png"/>
<poco:preferredUsername>pekorino</poco:preferredUsername>
<poco:displayName></poco:displayName>
<poco:note>http://shitposter.club/mono 孤独のグルメ</poco:note>
</activity:object>
<link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1079786"/>
<ostatus:conversation href="https://social.heldscal.la/conversation/1079786" local_id="1079786" ref="tag:social.heldscal.la,2017-05-07:objectType=thread:nonce=6e80caf94e03029f">tag:social.heldscal.la,2017-05-07:objectType=thread:nonce=6e80caf94e03029f</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2092981.atom"/>
<link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2092981.atom"/>
<statusnet:notice_info local_id="2092981" source="activity"></statusnet:notice_info>
</entry>
</feed>

77
test/list_test.exs Normal file
View file

@ -0,0 +1,77 @@
defmodule Pleroma.ListTest do
alias Pleroma.{User, Repo}
use Pleroma.DataCase
import Pleroma.Factory
import Ecto.Query
test "creating a list" do
user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
%Pleroma.List{title: title} = Pleroma.List.get(list.id, user)
assert title == "title"
end
test "getting a list not belonging to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
ret = Pleroma.List.get(list.id, other_user)
assert is_nil(ret)
end
test "adding an user to a list" do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user)
assert [other_user.follower_address] == following
end
test "removing an user from a list" do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user)
{:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user)
assert [] == following
end
test "renaming a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, %{title: title}} = Pleroma.List.rename(list, "new")
assert "new" == title
end
test "deleting a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.delete(list)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
test "getting users in a list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
{:ok, following} = Pleroma.List.get_following(list)
assert other_user in following
assert third_user in following
end
test "getting all lists by an user" do
user = insert(:user)
other_user = insert(:user)
{:ok, list_one} = Pleroma.List.create("title", user)
{:ok, list_two} = Pleroma.List.create("other title", user)
{:ok, list_three} = Pleroma.List.create("third title", other_user)
lists = Pleroma.List.for_user(user, %{})
assert list_one in lists
assert list_two in lists
refute list_three in lists
end
end

View file

@ -122,7 +122,8 @@ def follow_activity_factory do
} }
%Pleroma.Activity{ %Pleroma.Activity{
data: data data: data,
actor: follower.ap_id
} }
end end

View file

@ -46,6 +46,15 @@ test "can't follow a deactivated users" do
{:error, _} = User.follow(user, followed) {:error, _} = User.follow(user, followed)
end end
test "can't follow a user who blocked us" do
blocker = insert(:user)
blockee = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
# This is a somewhat useless test. # This is a somewhat useless test.
# test "following a remote user will ensure a websub subscription is present" do # test "following a remote user will ensure a websub subscription is present" do
# user = insert(:user) # user = insert(:user)

View file

@ -425,7 +425,40 @@ test "creates an undo activity for the last follow" do
assert activity.data["type"] == "Undo" assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == follow_activity.data["id"]
assert is_map(activity.data["object"])
assert activity.data["object"]["type"] == "Follow"
assert activity.data["object"]["object"] == followed.ap_id
assert activity.data["object"]["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
assert is_map(activity.data["object"])
assert activity.data["object"]["type"] == "Block"
assert activity.data["object"]["object"] == blocked.ap_id
assert activity.data["object"]["id"] == block_activity.data["id"]
end end
end end

View file

@ -2,13 +2,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.Websub.WebsubClientSubscription alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.Websub.WebsubServerSubscription
import Ecto.Query
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -283,7 +282,7 @@ test "it works for incoming deletes" do
|> Map.put("object", object) |> Map.put("object", object)
|> Map.put("actor", activity.data["actor"]) |> Map.put("actor", activity.data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
refute Repo.get(Activity, activity.id) refute Repo.get(Activity, activity.id)
end end
@ -315,6 +314,234 @@ test "it works for incoming unannounces with an existing notice" do
assert data["object"]["id"] == assert data["object"]["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end end
test "it works for incomming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Poison.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_by_ap_id(data["actor"]), user)
end
test "it works for incoming blocks" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_by_ap_id(data["actor"])
assert User.blocks?(blocker, user)
end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Block"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user)
followed = insert(:user)
{:ok, follower} = User.follow(follower, followed)
assert User.following?(follower, followed) == true
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
object =
accept_data["object"]
|> Map.put("actor", follower.ap_id)
|> Map.put("id", follow_activity.data["id"])
accept_data = Map.put(accept_data, "object", object)
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
refute activity.local
assert activity.data["object"] == follow_activity.data["id"]
follower = Repo.get(User, follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which were orphaned" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = Repo.get(User, follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = Repo.get(User, follower.id)
assert User.following?(follower, followed) == true
end
test "it fails for incoming accepts which cannot be correlated" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = Repo.get(User, follower.id)
refute User.following?(follower, followed) == true
end
test "it fails for incoming rejects which cannot be correlated" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
accept_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = Repo.get(User, follower.id)
refute User.following?(follower, followed) == true
end
test "it works for incoming rejects which are orphaned" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
{:ok, follower} = User.follow(follower, followed)
{:ok, _follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
reject_data =
Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(reject_data)
refute activity.local
follower = Repo.get(User, follower.id)
assert User.following?(follower, followed) == false
end
test "it works for incoming rejects which are referenced by IRI only" do
follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}})
{:ok, follower} = User.follow(follower, followed)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
follower = Repo.get(User, follower.id)
assert User.following?(follower, followed) == false
end
end end
describe "prepare outgoing" do describe "prepare outgoing" do

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Web.CommonAPI.Test do
use Pleroma.DataCase
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "it de-duplicates tags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu #2HU"})
assert activity.data["object"]["tag"] == ["2hu"]
end
end

View file

@ -21,13 +21,12 @@ test "it adds attachment links to a given text and attachment set" do
test "incorrect password given" do test "incorrect password given" do
{:ok, user} = UserBuilder.insert() {:ok, user} = UserBuilder.insert()
assert Utils.confirm_current_password(user, %{"password" => ""}) == assert Utils.confirm_current_password(user, "") == {:error, "Invalid password."}
{:error, "Invalid password."}
end end
test "correct password given" do test "correct password given" do
{:ok, user} = UserBuilder.insert() {:ok, user} = UserBuilder.insert()
assert Utils.confirm_current_password(user, %{"password" => "test"}) == {:ok, user} assert Utils.confirm_current_password(user, "test") == {:ok, user}
end end
end end
end end

View file

@ -0,0 +1,19 @@
defmodule Pleroma.Web.MastodonAPI.ListViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.List
test "Represent a list" do
user = insert(:user)
title = "mortal enemies"
{:ok, list} = Pleroma.List.create(title, user)
expected = %{
id: to_string(list.id),
title: title
}
assert expected == ListView.render("list.json", %{list: list})
end
end

View file

@ -229,6 +229,125 @@ test "when you didn't create it", %{conn: conn} do
end end
end end
describe "lists" do
test "creating a list", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
test "adding users to a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
test "removing users from a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
test "listing users in a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "retrieving a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(list.id)
end
test "renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
test "deleting a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
test "list timeline", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity_two.id)
end
end
describe "notifications" do describe "notifications" do
test "list of notifications", %{conn: conn} do test "list of notifications", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -278,6 +278,30 @@ test "handle incoming follows" do
assert User.following?(follower, followed) assert User.following?(follower, followed)
end end
test "handle incoming unfollows with existing follow" do
incoming_follow = File.read!("test/fixtures/follow.xml")
{:ok, [_activity]} = OStatus.handle_incoming(incoming_follow)
incoming = File.read!("test/fixtures/unfollow.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Undo"
assert activity.data["id"] ==
"undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
assert is_map(activity.data["object"])
assert activity.data["object"]["type"] == "Follow"
assert activity.data["object"]["object"] == "https://pawoo.net/users/pekorino"
refute activity.local
follower = User.get_by_ap_id(activity.data["actor"])
followed = User.get_by_ap_id(activity.data["object"]["object"])
refute User.following?(follower, followed)
end
describe "new remote user creation" do describe "new remote user creation" do
test "returns local users" do test "returns local users" do
local_user = insert(:user) local_user = insert(:user)

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
alias Pleroma.Web.TwitterAPI.NotificationView alias Pleroma.Web.TwitterAPI.NotificationView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Comeonin.Pbkdf2
import Pleroma.Factory import Pleroma.Factory
@ -443,7 +444,7 @@ test "without valid credentials", %{conn: conn} do
test "with credentials", %{conn: conn, user: current_user} do test "with credentials", %{conn: conn, user: current_user} do
blocked = insert(:user) blocked = insert(:user)
{:ok, current_user} = User.block(current_user, blocked) {:ok, current_user, blocked} = TwitterAPI.block(current_user, %{"user_id" => blocked.id})
assert User.blocks?(current_user, blocked) assert User.blocks?(current_user, blocked)
conn = conn =
@ -801,6 +802,82 @@ test "Convert newlines to <br> in bio", %{conn: conn} do
assert user.bio == "Hello,<br>World! I<br> am a test." assert user.bio == "Hello,<br>World! I<br> am a test."
end end
describe "POST /api/pleroma/change_password" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/change_password")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials, valid password and new password and confirmation not matching", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with credentials, valid password and invalid new password", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
})
assert json_response(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with credentials, valid password and matching new password and confirmation", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = Repo.get(User, current_user.id)
assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do describe "POST /api/pleroma/delete_account" do
setup [:valid_user] setup [:valid_user]

View file

@ -166,7 +166,7 @@ test "Block another user using screen_name" do
test "Unblock another user using user_id" do test "Unblock another user using user_id" do
unblocked = insert(:user) unblocked = insert(:user)
user = insert(:user) user = insert(:user)
User.block(user, unblocked) {:ok, user, _unblocked} = TwitterAPI.block(user, %{"user_id" => unblocked.id})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id}) {:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
assert user.info["blocks"] == [] assert user.info["blocks"] == []
@ -175,7 +175,7 @@ test "Unblock another user using user_id" do
test "Unblock another user using screen_name" do test "Unblock another user using screen_name" do
unblocked = insert(:user) unblocked = insert(:user)
user = insert(:user) user = insert(:user)
User.block(user, unblocked) {:ok, user, _unblocked} = TwitterAPI.block(user, %{"screen_name" => unblocked.nickname})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname}) {:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
assert user.info["blocks"] == [] assert user.info["blocks"] == []

View file

@ -24,7 +24,7 @@ test "A follow notification" do
{:ok, follower} = User.follow(follower, user) {:ok, follower} = User.follow(follower, user)
{:ok, activity} = ActivityPub.follow(follower, user) {:ok, activity} = ActivityPub.follow(follower, user)
Cachex.set(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id))) Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
[follow_notif] = Notification.for_user(user) [follow_notif] = Notification.for_user(user)
represented = %{ represented = %{

View file

@ -31,7 +31,7 @@ test "A user" do
User.follow(second_follower, user) User.follow(second_follower, user)
User.follow(user, follower) User.follow(user, follower)
{:ok, user} = User.update_follower_count(user) {:ok, user} = User.update_follower_count(user)
Cachex.set(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id))) Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
image = "http://localhost:4001/images/avi.png" image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png" banner = "http://localhost:4001/images/banner.png"