Merge branch 'develop' into 'reactions'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
lain 2019-10-06 08:11:47 +00:00
commit 61097ba6ab
28 changed files with 592 additions and 187 deletions

View file

@ -56,6 +56,7 @@ Has these additional fields under the `pleroma` object:
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
- `deactivated`: boolean, true when the user is deactivated - `deactivated`: boolean, true when the user is deactivated
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
### Source ### Source

View file

@ -67,6 +67,8 @@ def create_or_bump_for(activity, opts \\ []) do
participations = participations =
Enum.map(users, fn user -> Enum.map(users, fn user ->
User.increment_unread_conversation_count(conversation, user)
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts) Participation.create_for_user_and_conversation(user, conversation, opts)

View file

@ -52,6 +52,15 @@ def mark_as_read(participation) do
participation participation
|> read_cng(%{read: true}) |> read_cng(%{read: true})
|> Repo.update() |> Repo.update()
|> case do
{:ok, participation} ->
participation = Repo.preload(participation, :user)
User.set_unread_conversation_count(participation.user)
{:ok, participation}
error ->
error
end
end end
def mark_as_unread(participation) do def mark_as_unread(participation) do
@ -135,4 +144,12 @@ def set_recipients(participation, user_ids) do
{:ok, Repo.preload(participation, :recipients, force: true)} {:ok, Repo.preload(participation, :recipients, force: true)}
end end
def unread_conversation_count_for_user(user) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
where: not p.read,
select: %{count: count(p.id)}
)
end
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Ecto.Multi alias Ecto.Multi
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
@ -842,6 +843,61 @@ def maybe_update_following_count(%User{local: false} = user) do
def maybe_update_following_count(user), do: user def maybe_update_following_count(user), do: user
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [
info:
fragment(
"jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
u.info,
p.count
)
]
)
|> where([u], u.id == ^user.id)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def set_unread_conversation_count(_), do: :noop
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
Participation.unread_conversation_count_for_user(user)
|> where([p], p.conversation_id == ^conversation.id)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [
info:
fragment(
"jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> where([u], u.id == ^user.id)
|> where([u, p], p.count == 0)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def increment_unread_conversation_count(_, _), do: :noop
def remove_duplicated_following(%User{following: following} = user) do def remove_duplicated_following(%User{following: following} = user) do
uniq_following = Enum.uniq(following) uniq_following = Enum.uniq(following)

View file

@ -47,6 +47,7 @@ defmodule Pleroma.User.Info do
field(:hide_followers, :boolean, default: false) field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true) field(:hide_favorites, :boolean, default: true)
field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: []) field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false}) field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil) field(:mascot, :map, default: nil)

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -291,8 +292,8 @@ def reject(%{to: to, actor: actor, object: object} = params) do
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
local = !(params[:local] == false) local = !(params[:local] == false)
activity_id = params[:activity_id]
with data <- %{ with data <- %{
"to" => to, "to" => to,
@ -301,6 +302,7 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
"actor" => actor, "actor" => actor,
"object" => object "object" => object
}, },
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}

View file

@ -601,7 +601,7 @@ def handle_incoming(
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_embedded_obj_helper(object_id, actor),
public <- Visibility.is_public?(data), public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity} {:ok, activity}
@ -642,7 +642,8 @@ def handle_incoming(
to: data["to"] || [], to: data["to"] || [],
cc: data["cc"] || [], cc: data["cc"] || [],
object: object, object: object,
actor: actor_id actor: actor_id,
activity_id: data["id"]
}) })
else else
e -> e ->
@ -824,6 +825,29 @@ def get_obj_helper(id, options \\ []) do
end end
end end
@spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
ap_id: ap_id
})
when attributed_to == ap_id do
with {:ok, activity} <-
handle_incoming(%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => attributed_to,
"object" => data
}) do
{:ok, Object.normalize(activity)}
else
_ -> get_obj_helper(object_id)
end
end
def get_embedded_obj_helper(object_id, _) do
get_obj_helper(object_id)
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(in_reply_to, "http"), with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do

View file

@ -830,6 +830,6 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> Repo.all() |> Repo.all()
end end
defp maybe_put(map, _key, nil), do: map def maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value) def maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -290,7 +292,7 @@ def update(user) do
ActivityPub.update(%{ ActivityPub.update(%{
local: true, local: true,
to: [user.follower_address], to: [Pleroma.Constants.as_public(), user.follower_address],
cc: [], cc: [],
actor: user.ap_id, actor: user.ap_id,
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})

View file

@ -105,6 +105,17 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup() |> Enum.dedup()
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
info_params = info_params =
[ [
:no_rich_text, :no_rich_text,
@ -122,12 +133,12 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end) end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "fields", :fields, fn fields -> |> add_if_present(params, "fields_attributes", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields} {:ok, fields}
end) end)
|> add_if_present(params, "fields", :raw_fields) |> add_if_present(params, "fields_attributes", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)} {:ok, Map.merge(user.info.pleroma_settings_store, value)}
end) end)
@ -168,6 +179,14 @@ defp add_if_present(map, params, params_field, map_field, value_function \\ &{:o
end end
end end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
fields
end
end
@doc "GET /api/v1/accounts/relationships" @doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
targets = User.get_all_by_ids(List.wrap(id)) targets = User.get_all_by_ids(List.wrap(id))
@ -301,4 +320,26 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
end end
end end
@doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
render(conn, "show.json", user: followed, for: follower)
else
{:followed, _} -> {:error, :not_found}
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
end
end end

View file

@ -5,86 +5,10 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Bookmark
alias Pleroma.Pagination
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
require Logger require Logger
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}
{:error, message} ->
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_accounts <- User.blocked_users(user) do
res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
json(conn, res)
end
end
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
# Stubs for unimplemented mastodon api # Stubs for unimplemented mastodon api
# #
def empty_array(conn, _) do def empty_array(conn, _) do

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [try_render: 3] import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
require Ecto.Query require Ecto.Query
@ -283,4 +283,39 @@ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
render(conn, "context.json", activity: activity, activities: activities, user: user) render(conn, "context.json", activity: activity, activities: activities, user: user)
end end
end end
@doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end
@doc "GET /api/v1/bookmarks"
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
user.id
|> Bookmark.for_user_query()
|> Pleroma.Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end end

View file

@ -167,6 +167,7 @@ defp do_render("show.json", %{user: user} = opts) do
|> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for]) |> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
end end
defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(string) when is_binary(string) do
@ -248,6 +249,16 @@ defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
defp maybe_put_activation_status(data, _, _), do: data defp maybe_put_activation_status(data, _, _), do: data
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
user.info.unread_conversation_count
)
end
defp maybe_put_unread_conversation_count(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View file

@ -355,14 +355,14 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", FollowRequestController, :index) get("/follow_requests", FollowRequestController, :index)
get("/blocks", MastodonAPIController, :blocks) get("/blocks", AccountController, :blocks)
get("/mutes", MastodonAPIController, :mutes) get("/mutes", AccountController, :mutes)
get("/timelines/home", TimelineController, :home) get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct) get("/timelines/direct", TimelineController, :direct)
get("/favourites", MastodonAPIController, :favourites) get("/favourites", StatusController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks) get("/bookmarks", StatusController, :bookmarks)
get("/notifications", NotificationController, :index) get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show) get("/notifications/:id", NotificationController, :show)
@ -434,7 +434,7 @@ defmodule Pleroma.Web.Router do
scope [] do scope [] do
pipe_through(:oauth_follow) pipe_through(:oauth_follow)
post("/follows", MastodonAPIController, :follows) post("/follows", AccountController, :follows)
post("/accounts/:id/follow", AccountController, :follow) post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow) post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block) post("/accounts/:id/block", AccountController, :block)

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddUnreadConversationCountToUserInfo do
use Ecto.Migration
def up do
execute("""
update users set info = jsonb_set(info, '{unread_conversation_count}', 0::varchar::jsonb, true) where local=true
""")
end
def down, do: :ok
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
test "getting a participation will also preload things" do test "getting a participation will also preload things" do
@ -30,6 +31,8 @@ test "for a new conversation, it sets the recipents of the participation" do
{:ok, activity} = {:ok, activity} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(user.id)
[participation] = Participation.for_user(user) [participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients) participation = Pleroma.Repo.preload(participation, :recipients)
@ -155,6 +158,7 @@ test "it sets recipients, always keeping the owner of the participation even whe
[participation] = Participation.for_user_with_last_activity_id(user) [participation] = Participation.for_user_with_last_activity_id(user)
participation = Repo.preload(participation, :recipients) participation = Repo.preload(participation, :recipients)
user = User.get_cached_by_id(user.id)
assert participation.recipients |> length() == 1 assert participation.recipients |> length() == 1
assert user in participation.recipients assert user in participation.recipients

View file

@ -0,0 +1,43 @@
{
"type": "Announce",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"published": "2018-02-17T19:39:15Z",
"object": {
"type": "Note",
"id": "https://mastodon.social/users/emelie/statuses/101849165031453404",
"attributedTo": "https://mastodon.social/users/emelie",
"content": "this is a public toot",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://mastodon.social/users/emelie",
"https://mastodon.social/users/emelie/followers"
]
},
"id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"cc": [
"http://mastodon.example.org/users/admin",
"http://mastodon.example.org/users/admin/followers"
],
"atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"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,35 @@
{
"type": "Announce",
"to": [
"http://mastodon.example.org/users/admin/followers"
],
"published": "2018-02-17T19:39:15Z",
"object": {
"type": "Note",
"id": "http://mastodon.example.org/@admin/99541947525187368",
"attributedTo": "http://mastodon.example.org/users/admin",
"content": "this is a private toot",
"to": [
"http://mastodon.example.org/users/admin/followers"
]
},
"id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"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

@ -46,6 +46,14 @@ def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _
}} }}
end end
def get("https://mastodon.social/users/emelie/statuses/101849165031453404", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://mastodon.social/users/emelie", _, _, _) do def get("https://mastodon.social/users/emelie", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
@ -349,6 +357,14 @@ def get(
}} }}
end end
def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://shitposter.club/notice/7369654", _, _, _) do def get("https://shitposter.club/notice/7369654", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{

View file

@ -479,6 +479,33 @@ test "it works for incoming announces with an existing activity" do
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
end end
test "it works for incoming announces with an inlined activity" do
data =
File.read!("test/fixtures/mastodon-announce-private.json")
|> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
object = Object.normalize(data["object"])
assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
assert object.data["content"] == "this is a private toot"
end
test "it rejects incoming announces with an inlined activity from another origin" do
data =
File.read!("test/fixtures/bogus-mastodon-announce.json")
|> Poison.decode!()
assert :error = Transmogrifier.handle_incoming(data)
end
test "it does not clobber the addressing on announce activities" do test "it does not clobber the addressing on announce activities" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
@ -597,6 +624,8 @@ test "it works for incoming update activities" do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
assert data["id"] == update_data["id"]
user = User.get_cached_by_ap_id(data["actor"]) user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle" assert user.name == "gargle"

View file

@ -14,6 +14,8 @@ defmodule Pleroma.Web.CommonAPITest do
import Pleroma.Factory import Pleroma.Factory
require Pleroma.Constants
clear_config([:instance, :safe_dm_mentions]) clear_config([:instance, :safe_dm_mentions])
clear_config([:instance, :limit]) clear_config([:instance, :limit])
clear_config([:instance, :max_pinned_statuses]) clear_config([:instance, :max_pinned_statuses])
@ -96,11 +98,13 @@ test "it adds emoji in the object" do
test "it adds emoji when updating profiles" do test "it adds emoji when updating profiles" do
user = insert(:user, %{name: ":firefox:"}) user = insert(:user, %{name: ":firefox:"})
CommonAPI.update(user) {:ok, activity} = CommonAPI.update(user)
user = User.get_cached_by_ap_id(user.ap_id) user = User.get_cached_by_ap_id(user.ap_id)
[firefox] = user.info.source_data["tag"] [firefox] = user.info.source_data["tag"]
assert firefox["name"] == ":firefox:" assert firefox["name"] == ":firefox:"
assert Pleroma.Constants.as_public() in activity.recipients
end end
describe "posting" do describe "posting" do

View file

@ -328,7 +328,7 @@ test "update fields", %{conn: conn} do
account = account =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200) |> json_response(200)
assert account["fields"] == [ assert account["fields"] == [
@ -344,6 +344,35 @@ test "update fields", %{conn: conn} do
%{"name" => "link", "value" => "cofe.io"} %{"name" => "link", "value" => "cofe.io"}
] ]
fields =
[
"fields_attributes[1][name]=link",
"fields_attributes[1][value]=cofe.io",
"fields_attributes[0][name]=<a href=\"http://google.com\">foo</a>",
"fields_attributes[0][value]=bar"
]
|> Enum.join("&")
account =
conn
|> put_req_header("content-type", "application/x-www-form-urlencoded")
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", fields)
|> json_response(200)
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
]
assert account["source"]["fields"] == [
%{
"name" => "<a href=\"http://google.com\">foo</a>",
"value" => "bar"
},
%{"name" => "link", "value" => "cofe.io"}
]
name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
@ -354,7 +383,7 @@ test "update fields", %{conn: conn} do
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> assign(:user, user) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403) |> json_response(403)
long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
@ -364,7 +393,7 @@ test "update fields", %{conn: conn} do
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> assign(:user, user) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403) |> json_response(403)
Pleroma.Config.put([:instance, :max_account_fields], 1) Pleroma.Config.put([:instance, :max_account_fields], 1)
@ -377,8 +406,23 @@ test "update fields", %{conn: conn} do
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> assign(:user, user) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403) |> json_response(403)
fields = [
%{"name" => "foo", "value" => ""},
%{"name" => "", "value" => "bar"}
]
account =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
assert account["fields"] == [
%{"name" => "foo", "value" => ""}
]
end end
end end
end end

View file

@ -849,4 +849,34 @@ test "returns an empty list on a bad request", %{conn: conn} do
assert [] = json_response(conn, 200) assert [] = json_response(conn, 200)
end end
end end
test "getting a list of mutes", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/mutes")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/blocks")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
end end

View file

@ -10,19 +10,23 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
import Pleroma.Factory import Pleroma.Factory
test "Conversations", %{conn: conn} do test "returns a list of conversations", %{conn: conn} do
user_one = insert(:user) user_one = insert(:user)
user_two = insert(:user) user_two = insert(:user)
user_three = insert(:user) user_three = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one) {:ok, user_two} = User.follow(user_two, user_one)
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
{:ok, direct} = {:ok, direct} =
CommonAPI.post(user_one, %{ CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!", "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
"visibility" => "direct" "visibility" => "direct"
}) })
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1
{:ok, _follower_only} = {:ok, _follower_only} =
CommonAPI.post(user_one, %{ CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!", "status" => "Hi @#{user_two.nickname}!",
@ -52,23 +56,100 @@ test "Conversations", %{conn: conn} do
assert is_binary(res_id) assert is_binary(res_id)
assert unread == true assert unread == true
assert res_last_status["id"] == direct.id assert res_last_status["id"] == direct.id
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
end
test "updates the last_status on reply", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}",
"visibility" => "direct"
})
{:ok, direct_reply} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
[%{"last_status" => res_last_status}] =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
assert res_last_status["id"] == direct_reply.id
end
test "the user marks a conversation as read", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}",
"visibility" => "direct"
})
[%{"id" => direct_conversation_id, "unread" => true}] =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
%{"unread" => false} =
conn
|> assign(:user, user_one)
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|> json_response(200)
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
# The conversation is marked as unread on reply
{:ok, _} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
[%{"unread" => true}] =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
{:ok, _} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
end
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
# Apparently undocumented API endpoint
res_conn = res_conn =
conn conn
|> assign(:user, user_one) |> assign(:user, user_one)
|> post("/api/v1/conversations/#{res_id}/read") |> get("/api/v1/statuses/#{direct.id}/context")
assert response = json_response(res_conn, 200)
assert length(response["accounts"]) == 2
assert response["last_status"]["id"] == direct.id
assert response["unread"] == false
# (vanilla) Mastodon frontend behaviour
res_conn =
conn
|> assign(:user, user_one)
|> get("/api/v1/statuses/#{res_last_status["id"]}/context")
assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
end end

View file

@ -1242,4 +1242,51 @@ test "context" do
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response } = response
end end
test "returns the favorites of a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
first_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
"status" =>
"Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
last_like = status["id"]
second_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?limit=0")
assert [] = json_response(third_conn, 200)
end
end end

View file

@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -20,36 +19,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
clear_config([:rich_media, :enabled]) clear_config([:rich_media, :enabled])
test "getting a list of mutes", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/mutes")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/blocks")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "unimplemented follow_requests, blocks, domain blocks" do test "unimplemented follow_requests, blocks, domain blocks" do
user = insert(:user) user = insert(:user)
@ -64,53 +33,6 @@ test "unimplemented follow_requests, blocks, domain blocks" do
end) end)
end end
test "returns the favorites of a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
first_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
"status" =>
"Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
last_like = status["id"]
second_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?limit=0")
assert [] = json_response(third_conn, 200)
end
describe "link headers" do describe "link headers" do
test "preserves parameters in link headers", %{conn: conn} do test "preserves parameters in link headers", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -418,6 +418,27 @@ test "shows actual follower/following count to the account owner" do
following_count: 1 following_count: 1
} = AccountView.render("show.json", %{user: user, for: user}) } = AccountView.render("show.json", %{user: user, for: user})
end end
test "shows unread_conversation_count only to the account owner" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "Hey @#{other_user.nickname}.",
"visibility" => "direct"
})
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
:unread_conversation_count
] == nil
assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
:unread_conversation_count
] == 1
end
end end
describe "follow requests counter" do describe "follow requests counter" do

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -133,6 +134,7 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
participation = Repo.preload(participation, :recipients) participation = Repo.preload(participation, :recipients)
user = User.get_cached_by_id(user.id)
assert [user] == participation.recipients assert [user] == participation.recipients
assert other_user not in participation.recipients assert other_user not in participation.recipients