diff --git a/.gitignore b/.gitignore index 082c7491b..9591f9976 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ erl_crash.dump # Prevent committing docs files /priv/static/doc/* + +# Code test coverage +/cover +/Elixir.*.coverdata diff --git a/CHANGELOG.md b/CHANGELOG.md index 210aae2e4..b9c9538b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Pleroma API: Healthcheck endpoint - Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for making users follow/unfollow each other +- Admin API: added filters (role, tags, email, name) for users endpoint - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) @@ -60,6 +61,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Deps: Updated Ecto to 3.0.7 - Don't ship finmoji by default, they can be installed as an emoji pack - Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. +- Admin API: Move the user related API to `api/pleroma/admin/users` ### Fixed - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. diff --git a/README.md b/README.md index 987f973ea..928f75dc7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For clients it supports both the [GNU Social API with Qvitter extensions](https: - [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html) -No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . +If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . ## Installation diff --git a/config/config.exs b/config/config.exs index 946ba9adf..1e64b79a7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -232,8 +232,7 @@ welcome_message: nil, max_report_comment_size: 1000, safe_dm_mentions: false, - healthcheck: false, - repo_batch_size: 500 + healthcheck: false config :pleroma, :markup, # XXX - unfortunately, inline images must be enabled by default right now, because diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 8befa8ea0..75fa2ee83 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -8,15 +8,20 @@ Authentication is required and the user must be an admin. - Method `GET` - Query Params: - - *optional* `query`: **string** search term + - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain) - *optional* `filters`: **string** comma-separated string of filters: - `local`: only local users - `external`: only external users - `active`: only active users - `deactivated`: only deactivated users + - `is_admin`: users with admin role + - `is_moderator`: users with moderator role - *optional* `page`: **integer** page number - *optional* `page_size`: **integer** number of users per page (default is `50`) -- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10` + - *optional* `tags`: **[string]** tags list + - *optional* `name`: **string** user display name + - *optional* `email`: **string** user email +- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` - Response: ```JSON @@ -40,7 +45,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/user` +## `/api/pleroma/admin/users` ### Remove a user @@ -58,7 +63,7 @@ Authentication is required and the user must be an admin. - `password` - Response: User’s nickname -## `/api/pleroma/admin/user/follow` +## `/api/pleroma/admin/users/follow` ### Make a user follow another user - Methods: `POST` @@ -68,7 +73,7 @@ Authentication is required and the user must be an admin. - Response: - "ok" -## `/api/pleroma/admin/user/unfollow` +## `/api/pleroma/admin/users/unfollow` ### Make a user unfollow another user - Methods: `POST` @@ -111,7 +116,7 @@ Authentication is required and the user must be an admin. - `nickname` - `tags` -## `/api/pleroma/admin/permission_group/:nickname` +## `/api/pleroma/admin/users/:nickname/permission_group` ### Get user user permission groups membership @@ -126,7 +131,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/permission_group/:nickname/:permission_group` +## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group` Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist. @@ -160,7 +165,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - On success: JSON of the `user.info` - Note: An admin cannot revoke their own admin status. -## `/api/pleroma/admin/activation_status/:nickname` +## `/api/pleroma/admin/users/:nickname/activation_status` ### Active or deactivate a user @@ -198,7 +203,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Response: - On success: URL of the unfollowed relay -## `/api/pleroma/admin/invite_token` +## `/api/pleroma/admin/users/invite_token` ### Get an account registration invite token @@ -210,7 +215,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ] - Response: invite token (base64 string) -## `/api/pleroma/admin/invites` +## `/api/pleroma/admin/users/invites` ### Get a list of generated invites @@ -236,7 +241,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` -## `/api/pleroma/admin/revoke_invite` +## `/api/pleroma/admin/users/revoke_invite` ### Revoke invite by token @@ -259,7 +264,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ``` -## `/api/pleroma/admin/email_invite` +## `/api/pleroma/admin/users/email_invite` ### Sends registration invite via email @@ -268,7 +273,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `email` - `name`, optional -## `/api/pleroma/admin/password_reset` +## `/api/pleroma/admin/users/:nickname/password_reset` ### Get a password reset token for a given nickname diff --git a/docs/config.md b/docs/config.md index d999952e1..43ea24d80 100644 --- a/docs/config.md +++ b/docs/config.md @@ -104,7 +104,6 @@ config :pleroma, Pleroma.Emails.Mailer, * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`) * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``. -* `repo_batch_size`: Repo batch size. The number of loaded rows from the database to the memory for processing chunks. E.g. deleting user statuses. ## :logger * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack @@ -170,13 +169,13 @@ config :pleroma, :frontend_configurations, These settings **need to be complete**, they will override the defaults. -NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below. +NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below. ## :fe __THIS IS DEPRECATED__ If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method. -Please **set this option to false** in your config like this: +Please **set this option to false** in your config like this: ```elixir config :pleroma, :fe, false diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index cced73226..5cb54c3ca 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -109,7 +109,7 @@ def run(["get-packs" | args]) do ]) ) - binary_archive = Tesla.get!(src_url).body + binary_archive = Tesla.get!(client(), src_url).body archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] @@ -137,7 +137,7 @@ def run(["get-packs" | args]) do ]) ) - files = Tesla.get!(files_url).body |> Poison.decode!() + files = Tesla.get!(client(), files_url).body |> Poison.decode!() IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -213,7 +213,7 @@ def run(["gen-pack", src]) do IO.puts("Downloading the pack and generating SHA256") - binary_archive = Tesla.get!(src).body + binary_archive = Tesla.get!(client(), src).body archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() IO.puts("SHA256 is #{archive_sha}") @@ -272,7 +272,7 @@ def run(["gen-pack", src]) do defp fetch_manifest(from) do Poison.decode!( if String.starts_with?(from, "http") do - Tesla.get!(from).body + Tesla.get!(client(), from).body else File.read!(from) end @@ -290,4 +290,12 @@ defp parse_global_opts(args) do ] ) end + + defp client do + middleware = [ + {Tesla.Middleware.FollowRedirects, [max_redirects: 3]} + ] + + Tesla.client(middleware) + end end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 73e63bb14..c121e800f 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,9 +6,11 @@ defmodule Pleroma.Activity do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.User import Ecto.Changeset import Ecto.Query @@ -35,6 +37,8 @@ defmodule Pleroma.Activity do field(:local, :boolean, default: true) field(:actor, :string) field(:recipients, {:array, :string}, default: []) + # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark + has_one(:bookmark, Bookmark) has_many(:notifications, Notification, on_delete: :delete_all) # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! @@ -73,6 +77,16 @@ def with_preloaded_object(query) do |> preload([activity, object], object: object) end + def with_preloaded_bookmark(query, %User{} = user) do + from([a] in query, + left_join: b in Bookmark, + on: b.user_id == ^user.id and b.activity_id == a.id, + preload: [bookmark: b] + ) + end + + def with_preloaded_bookmark(query, _), do: query + def get_by_ap_id(ap_id) do Repo.one( from( @@ -82,6 +96,16 @@ def get_by_ap_id(ap_id) do ) end + def get_bookmark(%Activity{} = activity, %User{} = user) do + if Ecto.assoc_loaded?(activity.bookmark) do + activity.bookmark + else + Bookmark.get(user.id, activity.id) + end + end + + def get_bookmark(_, _), do: nil + def change(struct, params \\ %{}) do struct |> cast(params, [:data]) @@ -263,6 +287,29 @@ def all_by_actor_and_id(actor, status_ids) do |> Repo.all() end + def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + from( + a in Activity, + where: + fragment( + "? ->> 'type' = 'Follow'", + a.data + ), + where: + fragment( + "? ->> 'state' = 'pending'", + a.data + ), + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + a.data, + a.data, + ^ap_id + ) + ) + end + @spec query_by_actor(actor()) :: Ecto.Query.t() def query_by_actor(actor) do from(a in Activity, where: a.actor == ^actor) diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex new file mode 100644 index 000000000..0db195988 --- /dev/null +++ b/lib/pleroma/conversation.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation do + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + use Ecto.Schema + import Ecto.Changeset + + schema "conversations" do + # This is the context ap id. + field(:ap_id, :string) + has_many(:participations, Participation) + has_many(:users, through: [:participations, :user]) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:ap_id]) + |> validate_required([:ap_id]) + |> unique_constraint(:ap_id) + end + + def create_for_ap_id(ap_id) do + %__MODULE__{} + |> creation_cng(%{ap_id: ap_id}) + |> Repo.insert( + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + returning: true, + conflict_target: :ap_id + ) + end + + def get_for_ap_id(ap_id) do + Repo.get_by(__MODULE__, ap_id: ap_id) + end + + @doc """ + This will + 1. Create a conversation if there isn't one already + 2. Create a participation for all the people involved who don't have one already + 3. Bump all relevant participations to 'unread' + """ + def create_or_bump_for(activity) do + with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), + "Create" <- activity.data["type"], + object <- Pleroma.Object.normalize(activity), + "Note" <- object.data["type"], + ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do + {:ok, conversation} = create_for_ap_id(ap_id) + + users = User.get_users_from_set(activity.recipients, false) + + participations = + Enum.map(users, fn user -> + {:ok, participation} = + Participation.create_for_user_and_conversation(user, conversation) + + participation + end) + + {:ok, + %{ + conversation + | participations: participations + }} + else + e -> {:error, e} + end + end +end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex new file mode 100644 index 000000000..61021fb18 --- /dev/null +++ b/lib/pleroma/conversation/participation.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation do + use Ecto.Schema + alias Pleroma.Conversation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + import Ecto.Changeset + import Ecto.Query + + schema "conversation_participations" do + belongs_to(:user, User, type: Pleroma.FlakeId) + belongs_to(:conversation, Conversation) + field(:read, :boolean, default: false) + field(:last_activity_id, Pleroma.FlakeId, virtual: true) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :conversation_id]) + |> validate_required([:user_id, :conversation_id]) + end + + def create_for_user_and_conversation(user, conversation) do + %__MODULE__{} + |> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) + |> Repo.insert( + on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], + returning: true, + conflict_target: [:user_id, :conversation_id] + ) + end + + def read_cng(struct, params) do + struct + |> cast(params, [:read]) + |> validate_required([:read]) + end + + def mark_as_read(participation) do + participation + |> read_cng(%{read: true}) + |> Repo.update() + end + + def mark_as_unread(participation) do + participation + |> read_cng(%{read: false}) + |> Repo.update() + end + + def for_user(user, params \\ %{}) do + from(p in __MODULE__, + where: p.user_id == ^user.id, + order_by: [desc: p.updated_at] + ) + |> Pleroma.Pagination.fetch_paginated(params) + |> Repo.preload(conversation: [:users]) + end + + def for_user_with_last_activity_id(user, params \\ %{}) do + for_user(user, params) + |> Enum.map(fn participation -> + activity_id = + ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) + + %{ + participation + | last_activity_id: activity_id + } + end) + end +end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 25bd911fb..2f4687fa2 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,7 +1,5 @@ defmodule Pleroma.Object.Containment do @moduledoc """ - # Object Containment - This module contains some useful functions for containing objects to specific origins and determining those origins. They previously lived in the ActivityPub `Transmogrifier` module. diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 2e7d747df..5b242927b 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -34,7 +34,7 @@ def schedule_update do def update_stats do peers = from( - u in Pleroma.User, + u in User, select: fragment("distinct split_part(?, '@', 2)", u.nickname), where: u.local != ^true ) @@ -44,10 +44,13 @@ def update_stats do domain_count = Enum.count(peers) status_query = - from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info)) + from(u in User.Query.build(%{local: true}), + select: fragment("sum((?->>'note_count')::int)", u.info) + ) status_count = Repo.one(status_query) - user_count = Repo.aggregate(User.active_local_user_query(), :count, :id) + + user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) Agent.update(__MODULE__, fn _ -> {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index f72334930..c47d65241 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Upload do @moduledoc """ - # Upload + Manage user uploads Options: * `:type`: presets for activity type (defaults to Document) and size limits from app configuration diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index fd2ce81ad..427400aa1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -10,7 +10,6 @@ defmodule Pleroma.User do alias Comeonin.Pbkdf2 alias Pleroma.Activity - alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Registration @@ -54,7 +53,6 @@ defmodule Pleroma.User do field(:search_type, :integer, virtual: true) field(:tags, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime_usec) - has_many(:bookmarks, Bookmark) has_many(:notifications, Notification) has_many(:registrations, Registration) embeds_one(:info, Pleroma.User.Info) @@ -256,10 +254,7 @@ defp autofollow_users(user) do candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) autofollowed_users = - from(u in User, - where: u.local == true, - where: u.nickname in ^candidates - ) + User.Query.build(%{nickname: candidates, local: true}) |> Repo.all() follow_all(user, autofollowed_users) @@ -578,19 +573,17 @@ def fetch_initial_posts(user) do ) end - def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do - from( - u in User, - where: fragment("? <@ ?", ^[follower_address], u.following), - where: u.id != ^id - ) + @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_followers_query(%User{} = user, nil) do + User.Query.build(%{followers: user}) end def get_followers_query(user, page) do from(u in get_followers_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_followers_query(User.t()) :: Ecto.Query.t() def get_followers_query(user), do: get_followers_query(user, nil) def get_followers(user, page \\ nil) do @@ -605,19 +598,17 @@ def get_followers_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_friends_query(%User{id: id, following: following}, nil) do - from( - u in User, - where: u.follower_address in ^following, - where: u.id != ^id - ) + @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_friends_query(%User{} = user, nil) do + User.Query.build(%{friends: user}) end def get_friends_query(user, page) do from(u in get_friends_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_friends_query(User.t()) :: Ecto.Query.t() def get_friends_query(user), do: get_friends_query(user, nil) def get_friends(user, page \\ nil) do @@ -632,33 +623,10 @@ def get_friends_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_follow_requests_query(%User{} = user) do - from( - a in Activity, - where: - fragment( - "? ->> 'type' = 'Follow'", - a.data - ), - where: - fragment( - "? ->> 'state' = 'pending'", - a.data - ), - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - a.data, - a.data, - ^user.ap_id - ) - ) - end - + @spec get_follow_requests(User.t()) :: {:ok, [User.t()]} def get_follow_requests(%User{} = user) do users = - user - |> User.get_follow_requests_query() + Activity.follow_requests_for_actor(user) |> join(:inner, [a], u in User, on: a.actor == u.ap_id) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> group_by([a, u], u.id) @@ -731,10 +699,7 @@ def update_note_count(%User{} = user) do def update_follower_count(%User{} = user) do follower_count_query = - User - |> where([u], ^user.follower_address in u.following) - |> where([u], u.id != ^user.id) - |> select([u], %{count: count(u.id)}) + User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)}) User |> where(id: ^user.id) @@ -757,38 +722,19 @@ def update_follower_count(%User{} = user) do end end - def get_users_from_set_query(ap_ids, false) do - from( - u in User, - where: u.ap_id in ^ap_ids - ) - end - - def get_users_from_set_query(ap_ids, true) do - query = get_users_from_set_query(ap_ids, false) - - from( - u in query, - where: u.local == true - ) - end - + @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do - get_users_from_set_query(ap_ids, local_only) + criteria = %{ap_id: ap_ids} + criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria + + User.Query.build(criteria) |> Repo.all() end + @spec get_recipients_from_activity(Activity.t()) :: [User.t()] def get_recipients_from_activity(%Activity{recipients: to}) do - query = - from( - u in User, - where: u.ap_id in ^to, - or_where: fragment("? && ?", u.following, ^to) - ) - - query = from(u in query, where: u.local == true) - - Repo.all(query) + User.Query.build(%{recipients_from_activity: to, local: true}) + |> Repo.all() end def search(query, resolve \\ false, for_user \\ nil) do @@ -1050,14 +996,23 @@ def subscribed_to?(user, %{ap_id: ap_id}) do end end - def muted_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes)) + @spec muted_users(User.t()) :: [User.t()] + def muted_users(user) do + User.Query.build(%{ap_id: user.info.mutes}) + |> Repo.all() + end - def blocked_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) + @spec blocked_users(User.t()) :: [User.t()] + def blocked_users(user) do + User.Query.build(%{ap_id: user.info.blocks}) + |> Repo.all() + end - def subscribers(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers)) + @spec subscribers(User.t()) :: [User.t()] + def subscribers(user) do + User.Query.build(%{ap_id: user.info.subscribers}) + |> Repo.all() + end def block_domain(user, domain) do info_cng = @@ -1083,69 +1038,6 @@ def unblock_domain(user, domain) do update_and_set_cache(cng) end - def maybe_local_user_query(query, local) do - if local, do: local_user_query(query), else: query - end - - def local_user_query(query \\ User) do - from( - u in query, - where: u.local == true, - where: not is_nil(u.nickname) - ) - end - - def maybe_external_user_query(query, external) do - if external, do: external_user_query(query), else: query - end - - def external_user_query(query \\ User) do - from( - u in query, - where: u.local == false, - where: not is_nil(u.nickname) - ) - end - - def maybe_active_user_query(query, active) do - if active, do: active_user_query(query), else: query - end - - def active_user_query(query \\ User) do - from( - u in query, - where: fragment("not (?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def maybe_deactivated_user_query(query, deactivated) do - if deactivated, do: deactivated_user_query(query), else: query - end - - def deactivated_user_query(query \\ User) do - from( - u in query, - where: fragment("(?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def active_local_user_query do - from( - u in local_user_query(), - where: fragment("not (?->'deactivated' @> 'true')", u.info) - ) - end - - def moderator_user_query do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_moderator' @> 'true'", u.info) - ) - end - def deactivate(%User{} = user, status \\ true) do info_cng = User.Info.set_activation_status(user.info, status) @@ -1308,7 +1200,7 @@ def ap_enabled?(%User{info: info}), do: info.ap_enabled def ap_enabled?(_), do: false @doc "Gets or fetch a user by uri or nickname." - @spec get_or_fetch(String.t()) :: User.t() + @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()} def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname) @@ -1425,22 +1317,12 @@ def error_user(ap_id) do } end + @spec all_superusers() :: [User.t()] def all_superusers do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) - ) + User.Query.build(%{super_users: true, local: true}) |> Repo.all() end - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end - def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex new file mode 100644 index 000000000..2dfe5ce92 --- /dev/null +++ b/lib/pleroma/user/query.ex @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Query do + @moduledoc """ + User query builder module. Builds query from new query or another user query. + + ## Example: + query = Pleroma.User.Query(%{nickname: "nickname"}) + another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"}) + Pleroma.Repo.all(query) + Pleroma.Repo.all(another_query) + + Adding new rules: + - *ilike criteria* + - add field to @ilike_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{nickname: "nickname"}) + - *equal criteria* + - add field to @equal_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{email: "email@example.com"}) + - *contains criteria* + - add field to @containns_criteria list + - pass values list + - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]}) + """ + import Ecto.Query + import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] + alias Pleroma.User + + @type criteria :: + %{ + query: String.t(), + tags: [String.t()], + name: String.t(), + email: String.t(), + local: boolean(), + external: boolean(), + active: boolean(), + deactivated: boolean(), + is_admin: boolean(), + is_moderator: boolean(), + super_users: boolean(), + followers: User.t(), + friends: User.t(), + recipients_from_activity: [String.t()], + nickname: [String.t()], + ap_id: [String.t()] + } + | %{} + + @ilike_criteria [:nickname, :name, :query] + @equal_criteria [:email] + @role_criteria [:is_admin, :is_moderator] + @contains_criteria [:ap_id, :nickname] + + @spec build(criteria()) :: Query.t() + def build(query \\ base_query(), criteria) do + prepare_query(query, criteria) + end + + @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t() + def paginate(query, page, page_size) do + from(u in query, + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + end + + defp base_query do + from(u in User) + end + + defp prepare_query(query, criteria) do + Enum.reduce(criteria, query, &compose_query/2) + end + + defp compose_query({key, value}, query) + when key in @ilike_criteria and not_empty_string(value) do + # hack for :query key + key = if key == :query, do: :nickname, else: key + where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) + end + + defp compose_query({key, value}, query) + when key in @equal_criteria and not_empty_string(value) do + where(query, [u], ^[{key, value}]) + end + + defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do + where(query, [u], field(u, ^key) in ^values) + end + + defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do + Enum.reduce(tags, query, &prepare_tag_criteria/2) + end + + defp compose_query({key, _}, query) when key in @role_criteria do + where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key))) + end + + defp compose_query({:super_users, _}, query) do + where( + query, + [u], + fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) + ) + end + + defp compose_query({:local, _}, query), do: location_query(query, true) + + defp compose_query({:external, _}, query), do: location_query(query, false) + + defp compose_query({:active, _}, query) do + where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:deactivated, _}, query) do + where(query, [u], fragment("?->'deactivated' @> 'true'", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do + where(query, [u], fragment("? <@ ?", ^[follower_address], u.following)) + |> where([u], u.id != ^id) + end + + defp compose_query({:friends, %User{id: id, following: following}}, query) do + where(query, [u], u.follower_address in ^following) + |> where([u], u.id != ^id) + end + + defp compose_query({:recipients_from_activity, to}, query) do + where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to)) + end + + defp compose_query(_unsupported_param, query), do: query + + defp prepare_tag_criteria(tag, query) do + or_where(query, [u], fragment("? = any(?)", ^tag, u.tags)) + end + + defp location_query(query, local) do + where(query, [u], u.local == ^local) + |> where([u], not is_nil(u.nickname)) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 483a2153f..8f8c23a9b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity + alias Pleroma.Conversation alias Pleroma.Instances alias Pleroma.Notification alias Pleroma.Object @@ -141,7 +142,14 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do end) Notification.create_notifications(activity) + + participations = + activity + |> Conversation.create_or_bump_for() + |> get_participations() + stream_out(activity) + stream_out_participations(participations) {:ok, activity} else %Activity{} = activity -> @@ -164,6 +172,19 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do end end + defp get_participations({:ok, %{participations: participations}}), do: participations + defp get_participations(_), do: [] + + def stream_out_participations(participations) do + participations = + participations + |> Repo.preload(:user) + + Enum.each(participations, fn participation -> + Pleroma.Web.Streamer.stream("participation", participation) + end) + end + def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" @@ -195,6 +216,7 @@ def stream_out(activity) do end end else + # TODO: Write test, replace with visibility test if !Enum.member?(activity.data["cc"] || [], public) && !Enum.member?( activity.data["to"], @@ -457,35 +479,44 @@ def flag( end end - def fetch_activities_for_context(context, opts \\ %{}) do + defp fetch_activities_for_context_query(context, opts) do public = ["https://www.w3.org/ns/activitystreams#Public"] recipients = if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public - query = from(activity in Activity) - - query = - query - |> restrict_blocked(opts) - |> restrict_recipients(recipients, opts["user"]) - - query = - from( - activity in query, - where: - fragment( - "?->>'type' = ? and ?->>'context' = ?", - activity.data, - "Create", - activity.data, - ^context - ), - order_by: [desc: :id] + from(activity in Activity) + |> restrict_blocked(opts) + |> restrict_recipients(recipients, opts["user"]) + |> where( + [activity], + fragment( + "?->>'type' = ? and ?->>'context' = ?", + activity.data, + "Create", + activity.data, + ^context ) - |> Activity.with_preloaded_object() + ) + |> order_by([activity], desc: activity.id) + end - Repo.all(query) + @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()] + def fetch_activities_for_context(context, opts \\ %{}) do + context + |> fetch_activities_for_context_query(opts) + |> Activity.with_preloaded_object() + |> Repo.all() + end + + @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) :: + Pleroma.FlakeId.t() | nil + def fetch_latest_activity_id_for_context(context, opts \\ %{}) do + context + |> fetch_activities_for_context_query(opts) + |> limit(1) + |> select([a], a.id) + |> Repo.one() end def fetch_public_activities(opts \\ %{}) do @@ -784,11 +815,32 @@ defp maybe_preload_objects(query, _) do |> Activity.with_preloaded_object() end + defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + + defp maybe_preload_bookmarks(query, opts) do + query + |> Activity.with_preloaded_bookmark(opts["user"]) + end + + defp maybe_order(query, %{order: :desc}) do + query + |> order_by(desc: :id) + end + + defp maybe_order(query, %{order: :asc}) do + query + |> order_by(asc: :id) + end + + defp maybe_order(query, _), do: query + def fetch_activities_query(recipients, opts \\ %{}) do base_query = from(activity in Activity) base_query |> maybe_preload_objects(opts) + |> maybe_preload_bookmarks(opts) + |> maybe_order(opts) |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) |> restrict_tag_reject(opts) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 34665a3a6..87fa514c3 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do alias Pleroma.User + @moduledoc "Prevent followbots from following with a bit of heuristic" + @behaviour Pleroma.Web.ActivityPub.MRF # XXX: this should become User.normalize_by_ap_id() or similar, really. diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index a93ccf386..b8d38aae6 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do require Logger + @moduledoc "Drop and log everything received" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 895376c9d..15d8514be 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do alias Pleroma.Object + @moduledoc "Ensure a re: is prepended on replies to a post with a Subject" @behaviour Pleroma.Web.ActivityPub.MRF @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 6736f3cb9..a699f6a7e 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do alias Pleroma.User + @moduledoc "Block messages with too much mentions (configurable)" + @behaviour Pleroma.Web.ActivityPub.MRF defp delist_message(message, threshold) when threshold > 0 do diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index e8dfba672..d5c341433 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do + @moduledoc "Reject or Word-Replace messages with a keyword or regex" + @behaviour Pleroma.Web.ActivityPub.MRF defp string_matches?(string, _) when not is_binary(string) do false diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index 081456046..f30fee0d5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do + @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex index 40f37bdb1..c47cb3298 100644 --- a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do + @moduledoc "Does nothing (lets the messages go through unmodified)" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 3d13cdb32..9c87c6963 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do + @moduledoc "Scrub configured hypertext markup" alias Pleroma.HTML @behaviour Pleroma.Web.ActivityPub.MRF diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 4197be847..ea3df1b4d 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do alias Pleroma.User + @moduledoc "Rejects non-public (followers-only, direct) activities" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 798ba9687..2f105700b 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do alias Pleroma.User + @moduledoc "Filter activities depending on their origin instance" @behaviour Pleroma.Web.ActivityPub.MRF defp check_accept(%{host: actor_host} = _actor_info, object) do diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index b242e44e6..b52be30e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -5,6 +5,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do alias Pleroma.User @behaviour Pleroma.Web.ActivityPub.MRF + @moduledoc """ + Apply policies based on user tags + + This policy applies policies on a user activities depending on their tags + on your instance. + + - `mrf_tag:media-force-nsfw`: Mark as sensitive on presence of attachments + - `mrf_tag:media-strip`: Remove attachments + - `mrf_tag:force-unlisted`: Mark as unlisted (removes from the federated timeline) + - `mrf_tag:sandbox`: Remove from public (local and federated) timelines + - `mrf_tag:disable-remote-subscription`: Reject non-local follow requests + - `mrf_tag:disable-any-subscription`: Reject any follow requests + """ defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(_), do: [] diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex index a3b1f8aa0..f5078d818 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do alias Pleroma.Config + @moduledoc "Accept-list of users from specified instances" @behaviour Pleroma.Web.ActivityPub.MRF defp filter_by_list(object, []), do: {:ok, object} diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 711f233a6..b553d96a8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -101,7 +101,10 @@ def list_users(conn, params) do search_params = %{ query: params["query"], page: page, - page_size: page_size + page_size: page_size, + tags: params["tags"], + name: params["name"], + email: params["email"] } with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), @@ -116,11 +119,11 @@ def list_users(conn, params) do ) end - @filters ~w(local external active deactivated) - - defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + @filters ~w(local external active deactivated is_admin is_moderator) @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} + defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + defp maybe_parse_filters(filters) do filters |> String.split(",") diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index 9a8e41c2a..ed919833e 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -10,45 +10,23 @@ defmodule Pleroma.Web.AdminAPI.Search do @page_size 50 - def user(%{query: term} = params) when is_nil(term) or term == "" do - query = maybe_filtered_query(params) + defmacro not_empty_string(string) do + quote do + is_binary(unquote(string)) and unquote(string) != "" + end + end + + @spec user(map()) :: {:ok, [User.t()], pos_integer()} + def user(params \\ %{}) do + query = User.Query.build(params) |> order_by([u], u.nickname) paginated_query = - maybe_filtered_query(params) - |> paginate(params[:page] || 1, params[:page_size] || @page_size) + User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size) - count = query |> Repo.aggregate(:count, :id) + count = Repo.aggregate(query, :count, :id) results = Repo.all(paginated_query) {:ok, results, count} end - - def user(%{query: term} = params) when is_binary(term) do - search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%")) - - count = search_query |> Repo.aggregate(:count, :id) - - results = - search_query - |> paginate(params[:page] || 1, params[:page_size] || @page_size) - |> Repo.all() - - {:ok, results, count} - end - - defp maybe_filtered_query(params) do - from(u in User, order_by: u.nickname) - |> User.maybe_local_user_query(params[:local]) - |> User.maybe_external_user_query(params[:external]) - |> User.maybe_active_user_query(params[:active]) - |> User.maybe_deactivated_user_query(params[:deactivated]) - end - - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 181483664..55706eeb8 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -10,12 +10,6 @@ defmodule Pleroma.Web.ControllerHelper do def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil def truthy_param?(value), do: value not in @falsy_param_values - def oauth_scopes(params, default) do - # Note: `scopes` is used by Mastodon — supporting it but sticking to - # OAuth's standard `scope` wherever we control it - Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default) - end - def json_response(conn, status, json) do conn |> put_status(status) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 7f939991d..9ef30e885 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -29,6 +29,13 @@ defmodule Pleroma.Web.Endpoint do # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength ) + plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") + + plug(Plug.Static, + at: "/pleroma/admin/", + from: {:pleroma, "priv/static/adminfe/"} + ) + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index b099199af..956736780 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Config + alias Pleroma.Conversation.Participation alias Pleroma.Filter alias Pleroma.Formatter alias Pleroma.Notification @@ -24,6 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AppView + alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -35,6 +37,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.MediaProxy alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.ControllerHelper @@ -48,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do action_fallback(:errors) def create_app(conn, params) do - scopes = ControllerHelper.oauth_scopes(params, ["read"]) + scopes = Scopes.fetch_scopes(params, ["read"]) app_attrs = params @@ -165,7 +168,7 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do end end - @mastodon_api_level "2.5.0" + @mastodon_api_level "2.6.5" def masto_instance(conn, _params) do instance = Config.get(:instance) @@ -293,8 +296,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do |> ActivityPub.contain_timeline(user) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:home_timeline, activities) |> put_view(StatusView) @@ -313,8 +314,6 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do |> ActivityPub.fetch_public_activities() |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) |> put_view(StatusView) @@ -322,8 +321,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do end def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_id(params["id"]), - reading_user <- Repo.preload(reading_user, :bookmarks) do + with %User{} = user <- User.get_cached_by_id(params["id"]) do activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -350,8 +348,6 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do |> ActivityPub.fetch_activities_query(params) |> Pagination.fetch_paginated(params) - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:dm_timeline, activities) |> put_view(StatusView) @@ -361,8 +357,6 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), true <- Visibility.visible_for_user?(activity, user) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user}) @@ -512,8 +506,6 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), %Activity{} = announce <- Activity.normalize(announce.data) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: announce, for: user, as: :activity}) @@ -523,8 +515,6 @@ def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -575,8 +565,6 @@ def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -588,8 +576,6 @@ def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -1110,8 +1096,6 @@ def favourites(%{assigns: %{user: user}} = conn, params) do ActivityPub.fetch_activities([], params) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:favourites, activities) |> put_view(StatusView) @@ -1157,7 +1141,6 @@ def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params def bookmarks(%{assigns: %{user: user}} = conn, params) do user = User.get_cached_by_id(user.id) - user = Repo.preload(user, bookmarks: :activity) bookmarks = Bookmark.for_user_query(user.id) @@ -1165,7 +1148,7 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do activities = bookmarks - |> Enum.map(fn b -> b.activity end) + |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) conn |> add_link_headers(:bookmarks, bookmarks) @@ -1274,8 +1257,6 @@ def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> render("index.json", %{activities: activities, for: user, as: :activity}) @@ -1712,6 +1693,31 @@ def reports(%{assigns: %{user: user}} = conn, params) do end end + def conversations(%{assigns: %{user: user}} = conn, params) do + participations = Participation.for_user_with_last_activity_id(user, params) + + conversations = + Enum.map(participations, fn participation -> + ConversationView.render("participation.json", %{participation: participation, user: user}) + end) + + conn + |> add_link_headers(:conversations, participations) + |> json(conversations) + end + + def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- + Repo.get_by(Participation, id: participation_id, user_id: user.id), + {:ok, participation} <- Participation.mark_as_read(participation) do + participation_view = + ConversationView.render("participation.json", %{participation: participation, user: user}) + + conn + |> json(participation_view) + end + end + def try_render(conn, target, params) when is_binary(target) do res = render(conn, target, params) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex new file mode 100644 index 000000000..8e8f7cf31 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -0,0 +1,38 @@ +defmodule Pleroma.Web.MastodonAPI.ConversationView do + use Pleroma.Web, :view + + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("participation.json", %{participation: participation, user: user}) do + participation = Repo.preload(participation, conversation: :users) + + last_activity_id = + with nil <- participation.last_activity_id do + ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) + end + + activity = Activity.get_by_id_with_object(last_activity_id) + + last_status = StatusView.render("status.json", %{activity: activity, for: user}) + + accounts = + AccountView.render("accounts.json", %{ + users: participation.conversation.users, + as: :user + }) + + %{ + id: participation.id |> to_string(), + accounts: accounts, + unread: !participation.read, + last_status: last_status + } + end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 62d064d71..bd2372944 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -75,18 +75,22 @@ def render("index.json", opts) do def render( "status.json", - %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts + %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts ) do user = get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) + activity_object = Object.normalize(activity) + + reblogged_activity = + Activity.create_by_object_ap_id(activity_object.data["id"]) + |> Activity.with_preloaded_bookmark(opts[:for]) + |> Repo.one() - reblogged_activity = Activity.get_create_by_object_ap_id(object) reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity)) - activity_object = Object.normalize(activity) favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) - bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], reblogged_activity) + bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil mentions = activity.recipients @@ -96,8 +100,8 @@ def render( %{ id: to_string(activity.id), - uri: object, - url: object, + uri: activity_object.data["id"], + url: activity_object.data["id"], account: AccountView.render("account.json", %{user: user}), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -149,7 +153,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity) + bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil attachment_data = object.data["attachment"] || [] attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex index d2835a0ba..280cf28c0 100644 --- a/lib/pleroma/web/oauth.ex +++ b/lib/pleroma/web/oauth.ex @@ -3,18 +3,4 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth do - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> String.trim() - |> String.split(~r/[\s,]+/) - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index e3c01217d..8ee0da667 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -15,8 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - - import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] + alias Pleroma.Web.OAuth.Scopes if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) @@ -57,7 +56,7 @@ def authorize(conn, params), do: do_authorize(conn, params) defp do_authorize(conn, params) do app = Repo.get_by(App, client_id: params["client_id"]) available_scopes = (app && app.scopes) || [] - scopes = oauth_scopes(params, nil) || available_scopes + scopes = Scopes.fetch_scopes(params, available_scopes) # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template render(conn, Authenticator.auth_template(), %{ @@ -113,7 +112,7 @@ def after_create_authorization(conn, auth, %{ defp handle_create_authorization_error( conn, - {scopes_issue, _}, + {:error, scopes_issue}, %{"authorization" => _} = params ) when scopes_issue in [:unsupported_scopes, :missing_scopes] do @@ -184,9 +183,7 @@ def token_exchange( %App{} = app <- get_app_from_request(conn, params), {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:user_active, true} <- {:user_active, !user.info.deactivated}, - scopes <- oauth_scopes(params, app.scopes), - [] <- scopes -- app.scopes, - true <- Enum.any?(scopes), + {:ok, scopes} <- validate_scopes(app, params), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, response_token(user, token)) @@ -247,8 +244,9 @@ defp bad_request(conn, _) do @doc "Prepares OAuth request to provider for Ueberauth" def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do scope = - oauth_scopes(auth_attrs, []) - |> Enum.join(" ") + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() state = auth_attrs @@ -326,7 +324,7 @@ def registration_details(conn, %{"authorization" => auth_attrs}) do client_id: auth_attrs["client_id"], redirect_uri: auth_attrs["redirect_uri"], state: auth_attrs["state"], - scopes: oauth_scopes(auth_attrs, []), + scopes: Scopes.fetch_scopes(auth_attrs, []), nickname: auth_attrs["nickname"], email: auth_attrs["email"] }) @@ -401,10 +399,7 @@ defp do_create_authorization( {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, %App{} = app <- Repo.get_by(App, client_id: client_id), true <- redirect_uri in String.split(app.redirect_uris), - scopes <- oauth_scopes(auth_attrs, []), - {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, - # Note: `scope` param is intentionally not optional in this context - {:missing_scopes, false} <- {:missing_scopes, scopes == []}, + {:ok, scopes} <- validate_scopes(app, auth_attrs), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end @@ -458,4 +453,12 @@ defp response_token(%User{} = user, token, opts \\ %{}) do } |> Map.merge(opts) end + + @spec validate_scopes(App.t(), map()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(app, params) do + params + |> Scopes.fetch_scopes(app.scopes) + |> Scopes.validates(app.scopes) + end end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex new file mode 100644 index 000000000..ad9dfb260 --- /dev/null +++ b/lib/pleroma/web/oauth/scopes.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + @doc """ + Fetch scopes from requiest params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map(), list()) :: list() + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validates(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validates([], _app_scopes), do: {:error, :missing_scopes} + def validates(nil, _app_scopes), do: {:error, :missing_scopes} + + def validates(scopes, app_scopes) do + case scopes -- app_scopes do + [] -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end +end diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 166691a09..95037125d 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -18,15 +18,18 @@ defp get_href(id) do end end - defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do - [ - {:"thr:in-reply-to", - [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} - ] + defp get_in_reply_to(activity) do + with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do + [ + {:"thr:in-reply-to", + [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} + ] + else + _ -> + [] + end end - defp get_in_reply_to(_), do: [] - defp get_mentions(to) do Enum.map(to, fn id -> cond do @@ -98,7 +101,7 @@ def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) []} end) - in_reply_to = get_in_reply_to(activity.data) + in_reply_to = get_in_reply_to(activity) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -146,7 +149,6 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -177,7 +179,6 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ff4f08af5..8b84fbbad 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -146,34 +146,52 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through([:admin_api, :oauth_write]) - post("/user/follow", AdminAPIController, :user_follow) - post("/user/unfollow", AdminAPIController, :user_unfollow) - - get("/users", AdminAPIController, :list_users) - get("/users/:nickname", AdminAPIController, :user_show) + post("/users/follow", AdminAPIController, :user_follow) + post("/users/unfollow", AdminAPIController, :user_unfollow) + # TODO: to be removed at version 1.0 delete("/user", AdminAPIController, :user_delete) - patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) post("/user", AdminAPIController, :user_create) + + delete("/users", AdminAPIController, :user_delete) + post("/users", AdminAPIController, :user_create) + patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) + # TODO: to be removed at version 1.0 get("/permission_group/:nickname", AdminAPIController, :right_get) get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get) post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add) delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete) - put("/activation_status/:nickname", AdminAPIController, :set_activation_status) + get("/users/:nickname/permission_group", AdminAPIController, :right_get) + get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) + post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add) + + delete( + "/users/:nickname/permission_group/:permission_group", + AdminAPIController, + :right_delete + ) + + put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status) post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) - get("/invite_token", AdminAPIController, :get_invite_token) - get("/invites", AdminAPIController, :invites) - post("/revoke_invite", AdminAPIController, :revoke_invite) - post("/email_invite", AdminAPIController, :email_invite) + get("/users/invite_token", AdminAPIController, :get_invite_token) + get("/users/invites", AdminAPIController, :invites) + post("/users/revoke_invite", AdminAPIController, :revoke_invite) + post("/users/email_invite", AdminAPIController, :email_invite) + # TODO: to be removed at version 1.0 get("/password_reset", AdminAPIController, :get_password_reset) + + get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) + + get("/users", AdminAPIController, :list_users) + get("/users/:nickname", AdminAPIController, :user_show) end scope "/", Pleroma.Web.TwitterAPI do @@ -276,6 +294,9 @@ defmodule Pleroma.Web.Router do get("/suggestions", MastodonAPIController, :suggestions) + get("/conversations", MastodonAPIController, :conversations) + post("/conversations/:id/read", MastodonAPIController, :conversation_read) + get("/endorsements", MastodonAPIController, :empty_array) get("/pleroma/flavour", MastodonAPIController, :get_flavour) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 72eaf2084..133decfc4 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do use GenServer require Logger alias Pleroma.Activity + alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User @@ -71,6 +72,15 @@ def handle_cast(%{action: :stream, topic: "direct", item: item}, topics) do {:noreply, topics} end + def handle_cast(%{action: :stream, topic: "participation", item: participation}, topics) do + user_topic = "direct:#{participation.user_id}" + Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") + + push_to_socket(topics, user_topic, participation) + + {:noreply, topics} + end + def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do # filter the recipient list if the activity is not public, see #270. recipient_lists = @@ -192,6 +202,19 @@ defp represent_update(%Activity{} = activity) do |> Jason.encode!() end + def represent_conversation(%Participation{} = participation) do + %{ + event: "conversation", + payload: + Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ + participation: participation, + user: participation.user + }) + |> Jason.encode!() + } + |> Jason.encode!() + end + def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. @@ -214,6 +237,12 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite end) end + def push_to_socket(topics, topic, %Participation{} = participation) do + Enum.each(topics[topic] || [], fn socket -> + send(socket.transport_pid, {:text, represent_conversation(participation)}) + end) + end + def push_to_socket(topics, topic, %Activity{ data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} }) do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index ef7b6fe65..21e6c555a 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -182,6 +182,7 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do |> Map.put("blocking_user", user) |> Map.put("user", user) |> Map.put(:visibility, "direct") + |> Map.put(:order, :desc) activities = ActivityPub.fetch_activities_query([user.ap_id], params) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index c64152da8..d084ad734 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -170,7 +170,7 @@ def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activ created_at = activity.data["published"] |> Utils.date_to_asctime() announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) - text = "#{user.nickname} retweeted a status." + text = "#{user.nickname} repeated a status." retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity})) diff --git a/mix.exs b/mix.exs index 47f7c2903..5600aaa42 100644 --- a/mix.exs +++ b/mix.exs @@ -16,11 +16,11 @@ def project do # Docs name: "Pleroma", - source_url: "https://git.pleroma.social/pleroma/pleroma", - source_url_pattern: - "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", homepage_url: "https://pleroma.social/", + source_url: "https://git.pleroma.social/pleroma/pleroma", docs: [ + source_url_pattern: + "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", logo: "priv/static/static/logo.png", extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"), groups_for_extras: [ @@ -87,7 +87,7 @@ defp deps do {:bbcode, "~> 0.1"}, {:ex_machina, "~> 2.3", only: :test}, {:credo, "~> 0.9.3", only: [:dev, :test]}, - {:mock, "~> 0.3.1", only: :test}, + {:mock, "~> 0.3.3", only: :test}, {:crypt, git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}, {:cors_plug, "~> 1.5"}, @@ -113,7 +113,8 @@ defp deps do {:recon, github: "ferd/recon", tag: "2.4.0"}, {:quack, "~> 0.1.1"}, {:benchee, "~> 1.0"}, - {:esshd, "~> 0.1.0"} + {:esshd, "~> 0.1.0"}, + {:plug_static_index_html, "~> 1.0.0"} ] ++ oauth_deps end diff --git a/mix.lock b/mix.lock index df4d31c2f..981cc1747 100644 --- a/mix.lock +++ b/mix.lock @@ -46,7 +46,7 @@ "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [: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.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, @@ -59,6 +59,7 @@ "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/priv/repo/migrations/20190408123347_create_conversations.exs b/priv/repo/migrations/20190408123347_create_conversations.exs new file mode 100644 index 000000000..0e0af30ae --- /dev/null +++ b/priv/repo/migrations/20190408123347_create_conversations.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateConversations do + use Ecto.Migration + + def change do + create table(:conversations) do + add(:ap_id, :string, null: false) + timestamps() + end + + create table(:conversation_participations) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:conversation_id, references(:conversations, on_delete: :delete_all)) + add(:read, :boolean, default: false) + + timestamps() + end + + create index(:conversation_participations, [:conversation_id]) + create unique_index(:conversation_participations, [:user_id, :conversation_id]) + create unique_index(:conversations, [:ap_id]) + end +end diff --git a/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs b/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs new file mode 100644 index 000000000..1ce688c52 --- /dev/null +++ b/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddParticipationUpdatedAtIndex do + use Ecto.Migration + + def change do + create index(:conversation_participations, ["updated_at desc"]) + end +end diff --git a/priv/static/adminfe/favicon.ico b/priv/static/adminfe/favicon.ico new file mode 100644 index 000000000..34b63ac63 Binary files /dev/null and b/priv/static/adminfe/favicon.ico differ diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html new file mode 100644 index 000000000..44a58a44e --- /dev/null +++ b/priv/static/adminfe/index.html @@ -0,0 +1 @@ +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/css/app.cea15678.css b/priv/static/adminfe/static/css/app.cea15678.css new file mode 100644 index 000000000..136aa8bb1 Binary files /dev/null and b/priv/static/adminfe/static/css/app.cea15678.css differ diff --git a/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css new file mode 100644 index 000000000..da819ca09 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css differ diff --git a/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css new file mode 100644 index 000000000..2a2fbd8e4 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css differ diff --git a/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css new file mode 100644 index 000000000..7fa43bf28 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css differ diff --git a/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css new file mode 100644 index 000000000..fbd926db1 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css differ diff --git a/priv/static/adminfe/static/css/chunk-f018.0d22684d.css b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css new file mode 100644 index 000000000..bdb738700 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css differ diff --git a/priv/static/adminfe/static/css/chunk-libs.bd17d456.css b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css new file mode 100644 index 000000000..3a7a99679 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css differ diff --git a/priv/static/adminfe/static/fonts/element-icons.2fad952.woff b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff new file mode 100644 index 000000000..28da65d49 Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff differ diff --git a/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf new file mode 100644 index 000000000..73bc90f4a Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf differ diff --git a/priv/static/adminfe/static/img/401.089007e.gif b/priv/static/adminfe/static/img/401.089007e.gif new file mode 100644 index 000000000..cd6e0d943 Binary files /dev/null and b/priv/static/adminfe/static/img/401.089007e.gif differ diff --git a/priv/static/adminfe/static/img/404.a57b6f3.png b/priv/static/adminfe/static/img/404.a57b6f3.png new file mode 100644 index 000000000..3d8e2305c Binary files /dev/null and b/priv/static/adminfe/static/img/404.a57b6f3.png differ diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js new file mode 100644 index 000000000..4387b8321 Binary files /dev/null and b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js differ diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js b/priv/static/adminfe/static/js/JEtC.f9ba4594.js new file mode 100644 index 000000000..504eaef1f Binary files /dev/null and b/priv/static/adminfe/static/js/JEtC.f9ba4594.js differ diff --git a/priv/static/adminfe/static/js/app.25699e3d.js b/priv/static/adminfe/static/js/app.25699e3d.js new file mode 100644 index 000000000..54694bf5e Binary files /dev/null and b/priv/static/adminfe/static/js/app.25699e3d.js differ diff --git a/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js new file mode 100644 index 000000000..1921d0f64 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js differ diff --git a/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js new file mode 100644 index 000000000..1b5614639 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js differ diff --git a/priv/static/adminfe/static/js/chunk-8b70.46525646.js b/priv/static/adminfe/static/js/chunk-8b70.46525646.js new file mode 100644 index 000000000..68b7ea1a3 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-8b70.46525646.js differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js new file mode 100644 index 000000000..1f40d7e84 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js differ diff --git a/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js new file mode 100644 index 000000000..9c06e442c Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js differ diff --git a/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js new file mode 100644 index 000000000..db0b5dc97 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js differ diff --git a/priv/static/adminfe/static/js/runtime.7144b2cf.js b/priv/static/adminfe/static/js/runtime.7144b2cf.js new file mode 100644 index 000000000..0a58ac351 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.7144b2cf.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js new file mode 100644 index 000000000..e11f322cc Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css new file mode 100644 index 000000000..128237fba Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif new file mode 100644 index 000000000..ba90cc36f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif new file mode 100644 index 000000000..74d897a4f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif new file mode 100644 index 000000000..963a96b8a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif new file mode 100644 index 000000000..c7cf1011d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif new file mode 100644 index 000000000..716f55e16 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif new file mode 100644 index 000000000..334d49e0e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif new file mode 100644 index 000000000..4efd549ed Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif new file mode 100644 index 000000000..82c5b182e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif new file mode 100644 index 000000000..ca2451e10 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif new file mode 100644 index 000000000..fe66220c2 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif new file mode 100644 index 000000000..fd27edfaa Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif new file mode 100644 index 000000000..0cc9bb71c Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif new file mode 100644 index 000000000..2075dc160 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif new file mode 100644 index 000000000..bef7e2573 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif new file mode 100644 index 000000000..0631c7616 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif new file mode 100644 index 000000000..648e6e879 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css new file mode 100644 index 000000000..96e4d7c5d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css new file mode 100644 index 000000000..7b45d3397 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css new file mode 100644 index 000000000..bad168cfe Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff new file mode 100644 index 000000000..1e3be038a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot new file mode 100644 index 000000000..b144ba0bd Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg new file mode 100644 index 000000000..b4ee6f408 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg @@ -0,0 +1,63 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf new file mode 100644 index 000000000..a983e2dc4 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff new file mode 100644 index 000000000..d8962df76 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot new file mode 100644 index 000000000..5336c38ff Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg new file mode 100644 index 000000000..9fa215f3d --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg @@ -0,0 +1,131 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf new file mode 100644 index 000000000..61a48a511 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff new file mode 100644 index 000000000..aace5d9c5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif new file mode 100644 index 000000000..606348c7f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif new file mode 100644 index 000000000..c69e93723 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif new file mode 100644 index 000000000..cccd7f023 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif new file mode 100644 index 000000000..388486517 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css new file mode 100644 index 000000000..4ad815bf5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map new file mode 100644 index 000000000..c8763dcc3 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["./src/skins/lightgray/main/less/desktop/Reset.less","./src/skins/lightgray/main/less/desktop/Variables.less","./src/skins/lightgray/main/less/desktop/Mixins.less","./src/skins/lightgray/main/less/desktop/Animations.less","./src/skins/lightgray/main/less/desktop/TinyMCE.less","./src/skins/lightgray/main/less/desktop/CropRect.less","./src/skins/lightgray/main/less/desktop/ImagePanel.less","./src/skins/lightgray/main/less/desktop/Arrows.less","./src/skins/lightgray/main/less/desktop/Sidebar.less","./src/skins/lightgray/main/less/desktop/Container.less","./src/skins/lightgray/main/less/desktop/Scrollable.less","./src/skins/lightgray/main/less/desktop/Panel.less","./src/skins/lightgray/main/less/desktop/FloatPanel.less","./src/skins/lightgray/main/less/desktop/Window.less","./src/skins/lightgray/main/less/desktop/ToolTip.less","./src/skins/lightgray/main/less/desktop/Progress.less","./src/skins/lightgray/main/less/desktop/Notification.less","./src/skins/lightgray/main/less/desktop/AbsoluteLayout.less","./src/skins/lightgray/main/less/desktop/Button.less","./src/skins/lightgray/main/less/desktop/ButtonGroup.less","./src/skins/lightgray/main/less/desktop/Checkbox.less","./src/skins/lightgray/main/less/desktop/ComboBox.less","./src/skins/lightgray/main/less/desktop/ColorBox.less","./src/skins/lightgray/main/less/desktop/ColorButton.less","./src/skins/lightgray/main/less/desktop/ColorPicker.less","./src/skins/lightgray/main/less/desktop/Path.less","./src/skins/lightgray/main/less/desktop/FieldSet.less","./src/skins/lightgray/main/less/desktop/FitLayout.less","./src/skins/lightgray/main/less/desktop/FlowLayout.less","./src/skins/lightgray/main/less/desktop/Iframe.less","./src/skins/lightgray/main/less/desktop/InfoBox.less","./src/skins/lightgray/main/less/desktop/Label.less","./src/skins/lightgray/main/less/desktop/MenuBar.less","./src/skins/lightgray/main/less/desktop/MenuButton.less","./src/skins/lightgray/main/less/desktop/MenuItem.less","./src/skins/lightgray/main/less/desktop/Throbber.less","./src/skins/lightgray/main/less/desktop/Menu.less","./src/skins/lightgray/main/less/desktop/ListBox.less","./src/skins/lightgray/main/less/desktop/ResizeHandle.less","./src/skins/lightgray/main/less/desktop/SelectBox.less","./src/skins/lightgray/main/less/desktop/Slider.less","./src/skins/lightgray/main/less/desktop/Spacer.less","./src/skins/lightgray/main/less/desktop/SplitButton.less","./src/skins/lightgray/main/less/desktop/StackLayout.less","./src/skins/lightgray/main/less/desktop/TabPanel.less","./src/skins/lightgray/main/less/desktop/TextBox.less","./src/skins/lightgray/main/less/desktop/DropZone.less","./src/skins/lightgray/main/less/desktop/BrowseButton.less","./src/skins/lightgray/main/less/desktop/Icons.less","./src/skins/lightgray/main/less/desktop/FilePicker.less"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js new file mode 100644 index 000000000..d7fcac80b Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js differ diff --git a/test/activity_test.exs b/test/activity_test.exs index ad889f544..7e91d534b 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.ActivityTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Bookmark import Pleroma.Factory test "returns an activity by it's AP id" do @@ -28,4 +29,48 @@ test "returns the activity that created an object" do assert activity == found_activity end + + test "preloading a bookmark" do + user = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + activity = insert(:note_activity) + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + {:ok, _bookmark2} = Bookmark.create(user2.id, activity.id) + {:ok, bookmark3} = Bookmark.create(user3.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user3) + |> Repo.one() + + assert queried_activity.bookmark == bookmark3 + end + + describe "getting a bookmark" do + test "when association is loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + + test "when association is not loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + end end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs new file mode 100644 index 000000000..568953b07 --- /dev/null +++ b/test/conversation/participation_test.exs @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.ParticipationTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + + test "it creates a participation for a conversation and a user" do + user = insert(:user) + conversation = insert(:conversation) + + {:ok, %Participation{} = participation} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.user_id == user.id + assert participation.conversation_id == conversation.id + + :timer.sleep(1000) + # Creating again returns the same participation + {:ok, %Participation{} = participation_two} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.id == participation_two.id + refute participation.updated_at == participation_two.updated_at + end + + test "recreating an existing participations sets it to unread" do + participation = insert(:participation, %{read: true}) + + {:ok, participation} = + Participation.create_for_user_and_conversation( + participation.user, + participation.conversation + ) + + refute participation.read + end + + test "it marks a participation as read" do + participation = insert(:participation, %{read: false}) + {:ok, participation} = Participation.mark_as_read(participation) + + assert participation.read + end + + test "it marks a participation as unread" do + participation = insert(:participation, %{read: true}) + {:ok, participation} = Participation.mark_as_unread(participation) + + refute participation.read + end + + test "gets all the participations for a user, ordered by updated at descending" do + user = insert(:user) + {:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"}) + :timer.sleep(1000) + {:ok, activity_two} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"}) + :timer.sleep(1000) + + {:ok, activity_three} = + CommonAPI.post(user, %{ + "status" => "x", + "visibility" => "direct", + "in_reply_to_status_id" => activity_one.id + }) + + assert [participation_one, participation_two] = Participation.for_user(user) + + object2 = Pleroma.Object.normalize(activity_two) + object3 = Pleroma.Object.normalize(activity_three) + + assert participation_one.conversation.ap_id == object3.data["context"] + assert participation_two.conversation.ap_id == object2.data["context"] + + # Pagination + assert [participation_one] = Participation.for_user(user, %{"limit" => 1}) + + assert participation_one.conversation.ap_id == object3.data["context"] + + # With last_activity_id + assert [participation_one] = + Participation.for_user_with_last_activity_id(user, %{"limit" => 1}) + + assert participation_one.last_activity_id == activity_three.id + end +end diff --git a/test/conversation_test.exs b/test/conversation_test.exs new file mode 100644 index 000000000..864b2eb03 --- /dev/null +++ b/test/conversation_test.exs @@ -0,0 +1,175 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConversationTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Conversation + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it creates a conversation for given ap_id" do + assert {:ok, %Conversation{} = conversation} = + Conversation.create_for_ap_id("https://some_ap_id") + + # Inserting again returns the same + assert {:ok, conversation_two} = Conversation.create_for_ap_id("https://some_ap_id") + assert conversation_two.id == conversation.id + end + + test "public posts don't create conversations" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = Conversation.get_for_ap_id(context) + + refute conversation + end + + test "it creates or updates a conversation and participations for a given DM" do + har = insert(:user) + jafnhar = insert(:user, local: false) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "direct"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> har.id == user_id end) + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(jafnhar, %{ + "status" => "Hey @#{har.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_two = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation_two.id == conversation.id + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(tridi, %{ + "status" => "Hey @#{har.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_three = + Conversation.get_for_ap_id(context) + |> Repo.preload([:participations, :users]) + + assert conversation_three.id == conversation.id + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + tridi.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + tridi.id == user_id + end) + end + + test "create_or_bump_for returns the conversation with participations" do + har = insert(:user) + jafnhar = insert(:user, local: false) + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "direct"}) + + {:ok, conversation} = Conversation.create_or_bump_for(activity) + + assert length(conversation.participations) == 2 + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "public"}) + + assert {:error, _} = Conversation.create_or_bump_for(activity) + end + + test "create_or_bump_for does not normalize objects before checking the activity type" do + note = insert(:note) + note_id = note.data["id"] + Repo.delete(note) + refute Object.get_by_ap_id(note_id) + + Tesla.Mock.mock(fn env -> + case env.url do + ^note_id -> + # TODO: add attributedTo and tag to the note factory + body = + note.data + |> Map.put("attributedTo", note.data["actor"]) + |> Map.put("tag", []) + |> Jason.encode!() + + %Tesla.Env{status: 200, body: body} + end + end) + + undo = %Activity{ + id: "fake", + data: %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => note.data["actor"], + "to" => [note.data["actor"]], + "object" => note_id, + "type" => "Undo" + } + } + + Conversation.create_or_bump_for(undo) + + refute Object.get_by_ap_id(note_id) + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index ea59912cf..2a2954ad6 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -5,6 +5,23 @@ defmodule Pleroma.Factory do use ExMachina.Ecto, repo: Pleroma.Repo + def participation_factory do + conversation = insert(:conversation) + user = insert(:user) + + %Pleroma.Conversation.Participation{ + conversation: conversation, + user: user, + read: false + } + end + + def conversation_factory do + %Pleroma.Conversation{ + ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}") + } + end + def user_factory do user = %Pleroma.User{ name: sequence(:name, &"Test テスト User #{&1}"), diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index f8e987e58..1e056b7ee 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -22,6 +22,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do :ok end + describe "streaming out participations" do + test "it streams them out" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) + + participations = + conversation.participations + |> Repo.preload(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + ActivityPub.stream_out_participations(conversation.participations) + + Enum.each(participations, fn participation -> + assert called(Pleroma.Web.Streamer.stream("participation", participation)) + end) + end + end + end + describe "fetching restricted by visibility" do test "it restricts by the appropriate visibility" do user = insert(:user) @@ -130,9 +152,15 @@ test "drops activities beyond a certain limit" do end test "doesn't drop activities with content being null" do + user = insert(:user) + data = %{ - "ok" => true, + "actor" => user.ap_id, + "to" => [], "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", "content" => nil } } @@ -148,8 +176,17 @@ test "returns the activity if one with the same id is already in" do end test "inserts a given map into the activity database, giving it an id if it has none." do + user = insert(:user) + data = %{ - "ok" => true + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) @@ -159,9 +196,16 @@ test "inserts a given map into the activity database, giving it an id if it has given_id = "bla" data = %{ - "ok" => true, "id" => given_id, - "context" => "blabla" + "actor" => user.ap_id, + "to" => [], + "context" => "blabla", + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) @@ -172,26 +216,39 @@ test "inserts a given map into the activity database, giving it an id if it has end test "adds a context when none is there" do + user = insert(:user) + data = %{ - "id" => "some_id", + "actor" => user.ap_id, + "to" => [], "object" => %{ - "id" => "object_id" + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) + object = Pleroma.Object.normalize(activity) assert is_binary(activity.data["context"]) - assert is_binary(activity.data["object"]["context"]) + assert is_binary(object.data["context"]) assert activity.data["context_id"] - assert activity.data["object"]["context_id"] + assert object.data["context_id"] end test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do + user = insert(:user) + data = %{ + "actor" => user.ap_id, + "to" => [], "object" => %{ + "actor" => user.ap_id, + "to" => [], "type" => "Note", - "ok" => true + "content" => "hey" } } diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index b89c42327..6c1897b5a 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken import Pleroma.Factory - describe "/api/pleroma/admin/user" do + describe "/api/pleroma/admin/users" do test "Delete" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -18,7 +18,7 @@ test "Delete" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/user?nickname=#{user.nickname}") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") assert json_response(conn, 200) == user.nickname end @@ -30,7 +30,7 @@ test "Create" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user", %{ + |> post("/api/pleroma/admin/users", %{ "nickname" => "lain", "email" => "lain@example.org", "password" => "test" @@ -75,7 +75,7 @@ test "when the user doesn't exist", %{conn: conn} do end end - describe "/api/pleroma/admin/user/follow" do + describe "/api/pleroma/admin/users/follow" do test "allows to force-follow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -84,7 +84,7 @@ test "allows to force-follow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/follow", %{ + |> post("/api/pleroma/admin/users/follow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -96,7 +96,7 @@ test "allows to force-follow another user" do end end - describe "/api/pleroma/admin/user/unfollow" do + describe "/api/pleroma/admin/users/unfollow" do test "allows to force-unfollow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -107,7 +107,7 @@ test "allows to force-unfollow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/unfollow", %{ + |> post("/api/pleroma/admin/users/unfollow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -191,7 +191,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user end end - describe "/api/pleroma/admin/permission_group" do + describe "/api/pleroma/admin/users/:nickname/permission_group" do test "GET is giving user_info" do admin = insert(:user, info: %{is_admin: true}) @@ -199,7 +199,7 @@ test "GET is giving user_info" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/permission_group/#{admin.nickname}") + |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") assert json_response(conn, 200) == %{ "is_admin" => true, @@ -215,7 +215,7 @@ test "/:right POST, can add to a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => true @@ -230,7 +230,7 @@ test "/:right DELETE, can remove from a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => false @@ -238,7 +238,7 @@ test "/:right DELETE, can remove from a permission group" do end end - describe "PUT /api/pleroma/admin/activation_status" do + describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do setup %{conn: conn} do admin = insert(:user, info: %{is_admin: true}) @@ -255,7 +255,7 @@ test "deactivates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == true @@ -267,7 +267,7 @@ test "activates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == false @@ -280,7 +280,7 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, user) - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) assert json_response(conn, :forbidden) end @@ -309,7 +309,9 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}") + |> post( + "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" + ) assert json_response(conn, :no_content) @@ -341,13 +343,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, non_admin_user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :forbidden) end end - describe "POST /api/pleroma/admin/email_invite, with invalid config" do + describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do setup do [user: insert(:user, info: %{is_admin: true})] end @@ -367,7 +369,7 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -387,7 +389,7 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -405,7 +407,7 @@ test "/api/pleroma/admin/invite_token" do assert conn.status == 200 end - test "/api/pleroma/admin/password_reset" do + test "/api/pleroma/admin/users/:nickname/password_reset" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -413,20 +415,25 @@ test "/api/pleroma/admin/password_reset" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/password_reset?nickname=#{user.nickname}") + |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") assert conn.status == 200 end describe "GET /api/pleroma/admin/users" do - test "renders users array for the first page" do + setup do admin = insert(:user, info: %{is_admin: true}) - user = insert(:user, local: false, tags: ["foo", "bar"]) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=1") + + {:ok, conn: conn, admin: admin} + end + + test "renders users array for the first page", %{conn: conn, admin: admin} do + user = insert(:user, local: false, tags: ["foo", "bar"]) + conn = get(conn, "/api/pleroma/admin/users?page=1") assert json_response(conn, 200) == %{ "count" => 2, @@ -452,14 +459,10 @@ test "renders users array for the first page" do } end - test "renders empty array for the second page" do - admin = insert(:user, info: %{is_admin: true}) + test "renders empty array for the second page", %{conn: conn} do insert(:user) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=2") + conn = get(conn, "/api/pleroma/admin/users?page=2") assert json_response(conn, 200) == %{ "count" => 2, @@ -468,14 +471,10 @@ test "renders empty array for the second page" do } end - test "regular search" do - admin = insert(:user, info: %{is_admin: true}) + test "regular search", %{conn: conn} do user = insert(:user, nickname: "bob") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=bo") + conn = get(conn, "/api/pleroma/admin/users?query=bo") assert json_response(conn, 200) == %{ "count" => 1, @@ -493,17 +492,101 @@ test "regular search" do } end - test "regular search with page size" do - admin = insert(:user, info: %{is_admin: true}) + test "search by domain", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by full nickname", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by display name", %{conn: conn} do + user = insert(:user, name: "Display name") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?name=display") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by email", %{conn: conn} do + user = insert(:user, email: "email@example.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "regular search with page size", %{conn: conn} do user = insert(:user, nickname: "aalice") user2 = insert(:user, nickname: "alice") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=1") + conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - assert json_response(conn, 200) == %{ + assert json_response(conn1, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -518,12 +601,9 @@ test "regular search with page size" do ] } - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=2") + conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - assert json_response(conn, 200) == %{ + assert json_response(conn2, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -566,7 +646,7 @@ test "only local users" do } end - test "only local users with no query" do + test "only local users with no query", %{admin: old_admin} do admin = insert(:user, info: %{is_admin: true}, nickname: "john") user = insert(:user, nickname: "bob") @@ -578,7 +658,7 @@ test "only local users with no query" do |> get("/api/pleroma/admin/users?filters=local") assert json_response(conn, 200) == %{ - "count" => 2, + "count" => 3, "page_size" => 50, "users" => [ %{ @@ -596,6 +676,100 @@ test "only local users with no query" do "roles" => %{"admin" => true, "moderator" => false}, "local" => true, "tags" => [] + }, + %{ + "deactivated" => false, + "id" => old_admin.id, + "local" => true, + "nickname" => old_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "tags" => [] + } + ] + } + end + + test "load only admins", %{conn: conn, admin: admin} do + second_admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => admin.local, + "tags" => [] + }, + %{ + "deactivated" => false, + "id" => second_admin.id, + "nickname" => second_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => second_admin.local, + "tags" => [] + } + ] + } + end + + test "load only moderators", %{conn: conn} do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => moderator.id, + "nickname" => moderator.nickname, + "roles" => %{"admin" => false, "moderator" => true}, + "local" => moderator.local, + "tags" => [] + } + ] + } + end + + test "load users with tags list", %{conn: conn} do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => user1.id, + "nickname" => user1.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user1.local, + "tags" => ["first"] + }, + %{ + "deactivated" => false, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user2.local, + "tags" => ["second"] } ] } @@ -650,14 +824,19 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do } end - describe "GET /api/pleroma/admin/invite_token" do - test "without options" do + describe "GET /api/pleroma/admin/users/invite_token" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token") + + {:ok, conn: conn} + end + + test "without options", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invite_token") token = json_response(conn, 200) invite = UserInviteToken.find_by_token!(token) @@ -667,13 +846,9 @@ test "without options" do assert invite.invite_type == "one_time" end - test "with expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"expires_at" => Date.to_string(Date.utc_today())} }) @@ -686,13 +861,9 @@ test "with expires_at" do assert invite.invite_type == "date_limited" end - test "with max_use" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max_use", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150} }) @@ -704,13 +875,9 @@ test "with max_use" do assert invite.invite_type == "reusable" end - test "with max use and expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max use and expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())} }) @@ -723,26 +890,27 @@ test "with max use and expires_at" do end end - describe "GET /api/pleroma/admin/invites" do - test "no invites" do + describe "GET /api/pleroma/admin/users/invites" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + + {:ok, conn: conn} + end + + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{"invites" => []} end - test "with invite" do - admin = insert(:user, info: %{is_admin: true}) + test "with invite", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{ "invites" => [ @@ -760,7 +928,7 @@ test "with invite" do end end - describe "POST /api/pleroma/admin/revoke_invite" do + describe "POST /api/pleroma/admin/users/revoke_invite" do test "with token" do admin = insert(:user, info: %{is_admin: true}) {:ok, invite} = UserInviteToken.create_invite() @@ -768,7 +936,7 @@ test "with token" do conn = build_conn() |> assign(:user, admin) - |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token}) + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) assert json_response(conn, 200) == %{ "expires_at" => nil, diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs index 3950996ed..501a8d007 100644 --- a/test/web/admin_api/search_test.exs +++ b/test/web/admin_api/search_test.exs @@ -70,11 +70,11 @@ test "it returns active/deactivated users" do test "it returns specific user" do insert(:user) insert(:user) - insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) + user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) {:ok, _results, total_count} = Search.user(%{query: ""}) - {:ok, _results, count} = + {:ok, [^user], count} = Search.user(%{ query: "Bo", active: true, @@ -84,5 +84,87 @@ test "it returns specific user" do assert total_count == 3 assert count == 1 end + + test "it returns user by domain" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it return user by full nickname" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it returns admin user" do + admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^admin], count} = Search.user(%{is_admin: true}) + assert total == 3 + assert count == 1 + end + + test "it returns moderator user" do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) + assert total == 3 + assert count == 1 + end + + test "it returns users with tags" do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) + assert total == 4 + assert count == 2 + assert user1 in users + assert user2 in users + end + + test "it returns user by display name" do + user = insert(:user, name: "Display name") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{name: "display"}) + + assert total == 3 + assert count == 1 + end + + test "it returns user by email" do + user = insert(:user, email: "some@example.com") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) + + assert total == 3 + assert count == 1 + end end end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 610aa486e..505e45010 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -300,6 +300,65 @@ test "direct timeline", %{conn: conn} do assert status["url"] != direct.data["id"] end + test "Conversations", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, direct} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "direct" + }) + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "private" + }) + + res_conn = + conn + |> assign(:user, user_one) + |> get("/api/v1/conversations") + + assert response = json_response(res_conn, 200) + + assert [ + %{ + "id" => res_id, + "accounts" => res_accounts, + "last_status" => res_last_status, + "unread" => unread + } + ] = response + + assert length(res_accounts) == 2 + assert is_binary(res_id) + assert unread == true + assert res_last_status["id"] == direct.id + + # Apparently undocumented API endpoint + res_conn = + conn + |> assign(:user, user_one) + |> post("/api/v1/conversations/#{res_id}/read") + + 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) + end + test "doesn't include DMs from blocked users", %{conn: conn} do blocker = insert(:user) blocked = insert(:user) diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 5fddc6c58..d7c800e83 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -168,6 +168,8 @@ test "tells if the status is bookmarked" do {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + activity = Activity.get_by_id_with_object(activity.id) + status = StatusView.render("status.json", %{activity: activity, for: user}) assert status.bookmarked == true diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index a4bb68c4d..16ee02abb 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -67,37 +67,31 @@ test "a note activity" do end test "a reply note" do - note = insert(:note_activity) - answer = insert(:note_activity) - object = answer.data["object"] - object = Map.put(object, "inReplyTo", note.data["object"]["id"]) - - data = %{answer.data | "object" => object} - answer = %{answer | data: data} - - note_object = Object.get_by_ap_id(note.data["object"]["id"]) + user = insert(:user) + note_object = insert(:note) + _note = insert(:note_activity, %{note: note_object}) + object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}}) + answer = insert(:note_activity, %{note: object}) Repo.update!( Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")}) ) - user = User.get_cached_by_ap_id(answer.data["actor"]) - expected = """ http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post - #{answer.data["object"]["id"]} + #{object.data["id"]} New note by #{user.nickname} - #{answer.data["object"]["content"]} - #{answer.data["object"]["published"]} - #{answer.data["object"]["published"]} + #{object.data["content"]} + #{object.data["published"]} + #{object.data["published"]} #{answer.data["context"]} 2hu - - + + - + """ diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs index 1aa533b48..43bd77f78 100644 --- a/test/web/twitter_api/views/activity_view_test.exs +++ b/test/web/twitter_api/views/activity_view_test.exs @@ -295,8 +295,8 @@ test "an announce activity" do "id" => announce.id, "is_local" => true, "is_post_verb" => false, - "statusnet_html" => "shp retweeted a status.", - "text" => "shp retweeted a status.", + "statusnet_html" => "shp repeated a status.", + "text" => "shp repeated a status.", "uri" => "tag:#{announce.data["id"]}:objectType=note", "user" => UserView.render("show.json", user: other_user), "retweeted_status" => ActivityView.render("activity.json", activity: activity),