diff options
42 files changed, 747 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d516080b4..d7be06bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array - Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body). - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays +- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read +- Mastodon API: Add `/api/v1/markers` for managing timeline read markers ### Changed - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) diff --git a/config/config.exs b/config/config.exs index d0766a6e2..a69d41d17 100644 --- a/config/config.exs +++ b/config/config.exs @@ -322,6 +322,16 @@ config :pleroma, :assets, ], default_mascot: :pleroma_fox_tan +config :pleroma, :manifest, + icons: [ + %{ + src: "/static/logo.png", + type: "image/png" + } + ], + theme_color: "#282c37", + background_color: "#191b22" + config :pleroma, :activitypub, unfollow_blocked: true, outgoing_blocks: true, diff --git a/config/description.exs b/config/description.exs index 571c64bc1..70e963399 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1100,6 +1100,45 @@ config :pleroma, :config_description, [ }, %{ group: :pleroma, + key: :manifest, + type: :group, + description: + "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE", + children: [ + %{ + key: :icons, + type: {:list, :map}, + description: "Describe the icons of the app", + suggestion: [ + %{ + src: "/static/logo.png" + }, + %{ + src: "/static/icon.png", + type: "image/png" + }, + %{ + src: "/static/icon.ico", + sizes: "72x72 96x96 128x128 256x256" + } + ] + }, + %{ + key: :theme_color, + type: :string, + description: "Describe the theme color of the app", + suggestions: ["#282c37", "mediumpurple"] + }, + %{ + key: :background_color, + type: :string, + description: "Describe the background color of the app", + suggestions: ["#191b22", "aliceblue"] + } + ] + }, + %{ + group: :pleroma, key: :mrf_simple, type: :group, description: "Message Rewrite Facility", diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 0517bbdd7..6c326dc9b 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -367,6 +367,13 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * Response: JSON, statuses (200 - healthy, 503 unhealthy) +## `GET /api/v1/pleroma/conversations/read` +### Marks all user's conversations as read. +* Method `POST` +* Authentication: required +* Params: None +* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy). + ## `GET /api/pleroma/emoji/packs` ### Lists the custom emoji packs on the server * Method `GET` diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 8d85276ed..3427ae419 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -247,6 +247,35 @@ relates to mascots on the mastodon frontend * `default_mascot`: An element from `mascots` - This will be used as the default mascot on MastoFE (default: `:pleroma_fox_tan`) +## :manifest + +This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE. + +* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the + [spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it. + + Example: + + ```elixir + config :pleroma, :manifest, + icons: [ + %{ + src: "/static/logo.png" + }, + %{ + src: "/static/icon.png", + type: "image/png" + }, + %{ + src: "/static/icon.ico", + sizes: "72x72 96x96 128x128 256x256" + } + ] + ``` + +* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`) +* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`) + ## :mrf_simple * `media_removal`: List of instances to remove medias from * `media_nsfw`: List of instances to put medias as NSFW(sensitive) from diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 6ef0a635d..35669af27 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -111,19 +111,21 @@ defmodule Mix.Tasks.Pleroma.Emoji do file_list: files_to_unzip ) - IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name])) - - emoji_txt_str = - Enum.map( - files, - fn {shortcode, path} -> - emojo_path = Path.join("/emoji/#{pack_name}", path) - "#{shortcode}, #{emojo_path}" - end - ) - |> Enum.join("\n") - - File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str) + IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name])) + + pack_json = %{ + pack: %{ + "license" => pack["license"], + "homepage" => pack["homepage"], + "description" => pack["description"], + "fallback-src" => pack["src"], + "fallback-src-sha256" => pack["src_sha256"], + "share-files" => true + }, + files: files + } + + File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true)) else IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"])) end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index e17f49e58..41918fa78 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -69,6 +69,19 @@ defmodule Pleroma.Conversation.Participation do end end + def mark_all_as_read(user) do + {_, participations} = + __MODULE__ + |> where([p], p.user_id == ^user.id) + |> where([p], not p.read) + |> update([p], set: [read: true]) + |> select([p], p) + |> Repo.update_all([]) + + User.set_unread_conversation_count(user) + {:ok, participations} + end + def mark_as_unread(participation) do participation |> read_cng(%{read: false}) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex new file mode 100644 index 000000000..7f87c86c3 --- /dev/null +++ b/lib/pleroma/marker.ex @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Marker do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Ecto.Multi + alias Pleroma.Repo + alias Pleroma.User + + @timelines ["notifications"] + + schema "markers" do + field(:last_read_id, :string, default: "") + field(:timeline, :string, default: "") + field(:lock_version, :integer, default: 0) + + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + timestamps() + end + + def get_markers(user, timelines \\ []) do + Repo.all(get_query(user, timelines)) + end + + def upsert(%User{} = user, attrs) do + attrs + |> Map.take(@timelines) + |> Enum.reduce(Multi.new(), fn {timeline, timeline_attrs}, multi -> + marker = + user + |> get_marker(timeline) + |> changeset(timeline_attrs) + + Multi.insert(multi, timeline, marker, + returning: true, + on_conflict: {:replace, [:last_read_id]}, + conflict_target: [:user_id, :timeline] + ) + end) + |> Repo.transaction() + end + + defp get_marker(user, timeline) do + case Repo.find_resource(get_query(user, timeline)) do + {:ok, marker} -> %__MODULE__{marker | user: user} + _ -> %__MODULE__{timeline: timeline, user_id: user.id} + end + end + + @doc false + defp changeset(marker, attrs) do + marker + |> cast(attrs, [:last_read_id]) + |> validate_required([:user_id, :timeline, :last_read_id]) + |> validate_inclusion(:timeline, @timelines) + end + + defp by_timeline(query, timeline) do + from(m in query, where: m.timeline in ^List.wrap(timeline)) + end + + defp by_user_id(query, id), do: from(m in query, where: m.user_id == ^id) + + defp get_query(user, timelines) do + __MODULE__ + |> by_user_id(user.id) + |> by_timeline(timelines) + end +end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d145f8d5b..e5da1492b 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -55,9 +55,19 @@ defmodule Pleroma.Notification do ) |> preload([n, a, o], activity: {a, object: o}) |> exclude_muted(user, opts) + |> exclude_blocked(user) |> exclude_visibility(opts) end + defp exclude_blocked(query, user) do + query + |> where([n, a], a.actor not in ^user.info.blocks) + |> where( + [n, a], + fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks + ) + end + defp exclude_muted(query, _, %{with_muted: true}) do query end @@ -65,11 +75,6 @@ defmodule Pleroma.Notification do defp exclude_muted(query, user, _opts) do query |> where([n, a], a.actor not in ^user.info.muted_notifications) - |> where([n, a], a.actor not in ^user.info.blocks) - |> where( - [n, a], - fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks - ) |> join(:left, [n, a], tm in Pleroma.ThreadMute, on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) ) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ec705b8f6..2bbfaa55b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -88,6 +88,9 @@ defmodule Pleroma.User do def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true def superuser?(_), do: false + def invisible?(%User{info: %User.Info{invisible: true}}), do: true + def invisible?(_), do: false + def avatar_url(user, options \\ []) do case user.avatar do %{"url" => [%{"href" => href} | _]} -> href diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 2d39abcb3..982fb61c6 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -53,6 +53,7 @@ defmodule Pleroma.User.Info do field(:fields, {:array, :map}, default: nil) field(:raw_fields, {:array, :map}, default: []) field(:discoverable, :boolean, default: false) + field(:invisible, :boolean, default: false) field(:notification_settings, :map, default: %{ @@ -266,7 +267,8 @@ defmodule Pleroma.User.Info do :follower_count, :fields, :following_count, - :discoverable + :discoverable, + :invisible ]) |> validate_fields(true) end @@ -393,6 +395,14 @@ defmodule Pleroma.User.Info do |> validate_required([:source_data]) end + def set_invisible(info, invisible) do + params = %{invisible: invisible} + + info + |> cast(params, [:invisible]) + |> validate_required([:invisible]) + end + def admin_api_update(info, params) do info |> cast(params, [ diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 94c467b69..9a0a3522a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1106,6 +1106,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do locked = data["manuallyApprovesFollowers"] || false data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false + invisible = data["invisible"] || false user_data = %{ ap_id: data["id"], @@ -1115,7 +1116,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do banner: banner, fields: fields, locked: locked, - discoverable: discoverable + discoverable: discoverable, + invisible: invisible }, avatar: avatar, name: data["name"], diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 03fc434a9..de80612f1 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -10,8 +10,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do require Logger def get_actor do - "#{Pleroma.Web.Endpoint.url()}/relay" - |> User.get_or_create_service_actor_by_ap_id() + actor = + "#{Pleroma.Web.Endpoint.url()}/relay" + |> User.get_or_create_service_actor_by_ap_id() + + {:ok, actor} = User.update_info(actor, &User.Info.set_invisible(&1, true)) + actor end @spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()} diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 2c1ce9c55..4a250d131 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -596,13 +596,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do data, _options ) - when object_type in ["Person", "Application", "Service", "Organization"] do + when object_type in [ + "Person", + "Application", + "Service", + "Organization" + ] do with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) banner = new_user_data[:info][:banner] locked = new_user_data[:info][:locked] || false attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || [] + invisible = new_user_data[:info][:invisible] || false fields = attachment @@ -612,7 +618,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do update_data = new_user_data |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, %{banner: banner, locked: locked, fields: fields}) + |> Map.put(:info, %{banner: banner, locked: locked, fields: fields, invisible: invisible}) actor |> User.upgrade_changeset(update_data, true) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 4ef479f96..6b28df92c 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -491,10 +491,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do %Activity{data: %{"actor" => actor}}, object ) do - announcements = take_announcements(object) + unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do + announcements = take_announcements(object) - with announcements <- Enum.uniq([actor | announcements]) do - update_element_in_object("announcement", announcements, object) + with announcements <- Enum.uniq([actor | announcements]) do + update_element_in_object("announcement", announcements, object) + end + else + {:ok, object} end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 9b39d1629..8c5b4460b 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -55,7 +55,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do "owner" => user.ap_id, "publicKeyPem" => public_key }, - "endpoints" => endpoints + "endpoints" => endpoints, + "invisible" => User.invisible?(user) } |> Map.merge(Utils.make_json_ld_header()) end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 87860f1d5..93b38e8f4 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -34,6 +34,12 @@ defmodule Pleroma.Web.MastoFEController do end end + @doc "GET /web/manifest.json" + def manifest(conn, _params) do + conn + |> render("manifest.json") + end + @doc "PUT /api/web/settings" def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex new file mode 100644 index 000000000..ce025624d --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerController do + use Pleroma.Web, :controller + alias Pleroma.Plugs.OAuthScopesPlug + + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"]} + when action == :index + ) + + plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + # GET /api/v1/markers + def index(%{assigns: %{user: user}} = conn, params) do + markers = Pleroma.Marker.get_markers(user, params["timeline"]) + render(conn, "markers.json", %{markers: markers}) + end + + # POST /api/v1/markers + def upsert(%{assigns: %{user: user}} = conn, params) do + with {:ok, result} <- Pleroma.Marker.upsert(user, params), + markers <- Map.values(result) do + render(conn, "markers.json", %{markers: markers}) + end + end +end diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex new file mode 100644 index 000000000..38fbeed5f --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerView do + use Pleroma.Web, :view + + def render("markers.json", %{markers: markers}) do + Enum.reduce(markers, %{}, fn m, acc -> + Map.put_new(acc, m.timeline, %{ + last_read_id: m.last_read_id, + version: m.lock_version, + updated_at: NaiveDateTime.to_iso8601(m.updated_at) + }) + end) + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 9d50a7ca9..fc39abf05 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -79,6 +79,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end + def read_conversations(%{assigns: %{user: user}} = conn, _params) do + with {:ok, participations} <- Participation.mark_all_as_read(user) do + conn + |> add_link_headers(participations) + |> put_view(ConversationView) + |> render("participations.json", participations: participations, for: user) + end + end + def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do with {:ok, notification} <- Notification.read_one(user, notification_id) do conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d68fb87da..f69c5c2bc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -266,6 +266,7 @@ defmodule Pleroma.Web.Router do get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id", PleromaAPIController, :conversation) + post("/conversations/read", PleromaAPIController, :read_conversations) end scope [] do @@ -404,6 +405,9 @@ defmodule Pleroma.Web.Router do get("/push/subscription", SubscriptionController, :get) put("/push/subscription", SubscriptionController, :update) delete("/push/subscription", SubscriptionController, :delete) + + get("/markers", MarkerController, :index) + post("/markers", MarkerController, :upsert) end scope "/api/web", Pleroma.Web do @@ -592,6 +596,12 @@ defmodule Pleroma.Web.Router do end scope "/", Pleroma.Web do + pipe_through(:api) + + get("/web/manifest.json", MastoFEController, :manifest) + end + + scope "/", Pleroma.Web do pipe_through(:mastodon_html) get("/web/login", MastodonAPI.AuthController, :login) diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/masto_fe/index.html.eex index feff36fae..c330960fa 100644 --- a/lib/pleroma/web/templates/masto_fe/index.html.eex +++ b/lib/pleroma/web/templates/masto_fe/index.html.eex @@ -4,9 +4,13 @@ <meta charset='utf-8'> <meta content='width=device-width, initial-scale=1' name='viewport'> <title> -<%= Pleroma.Config.get([:instance, :name]) %> +<%= Config.get([:instance, :name]) %> </title> <link rel="icon" type="image/png" href="/favicon.png"/> +<link rel="manifest" type="applicaton/manifest+json" href="<%= masto_fe_path(Pleroma.Web.Endpoint, :manifest) %>" /> + +<meta name="theme-color" content="<%= Config.get([:manifest, :theme_color]) %>" /> + <script crossorigin='anonymous' src="/packs/locales.js"></script> <script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script> diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index 21b086d4c..85b164b59 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -99,4 +99,23 @@ defmodule Pleroma.Web.MastoFEView do defp present?(nil), do: false defp present?(false), do: false defp present?(_), do: true + + def render("manifest.json", _params) do + %{ + name: Config.get([:instance, :name]), + description: Config.get([:instance, :description]), + icons: Config.get([:manifest, :icons]), + theme_color: Config.get([:manifest, :theme_color]), + background_color: Config.get([:manifest, :background_color]), + display: "standalone", + scope: Pleroma.Web.base_url(), + start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), + categories: [ + "social" + ], + serviceworker: %{ + src: "/sw.js" + } + } + end end @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("1.0.0"), + version: version("1.1.50"), elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), diff --git a/priv/repo/migrations/20191014181019_create_markers.exs b/priv/repo/migrations/20191014181019_create_markers.exs new file mode 100644 index 000000000..c717831ba --- /dev/null +++ b/priv/repo/migrations/20191014181019_create_markers.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateMarkers do + use Ecto.Migration + + def change do + create_if_not_exists table(:markers) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:timeline, :string, default: "", null: false) + add(:last_read_id, :string, default: "", null: false) + add(:lock_version, :integer, default: 0, null: false) + timestamps() + end + + create_if_not_exists(unique_index(:markers, [:user_id, :timeline])) + end +end diff --git a/priv/repo/migrations/20191017225002_drop_websub_tables.exs b/priv/repo/migrations/20191017225002_drop_websub_tables.exs new file mode 100644 index 000000000..9e483b2a1 --- /dev/null +++ b/priv/repo/migrations/20191017225002_drop_websub_tables.exs @@ -0,0 +1,8 @@ +defmodule Pleroma.Repo.Migrations.DropWebsubTables do + use Ecto.Migration + + def change do + drop_if_exists(table(:websub_client_subscriptions)) + drop_if_exists(table(:websub_server_subscriptions)) + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 1cfcb7ec7..c8e69cab5 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -19,6 +19,7 @@ "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", + "invisible": "litepub:invisible", "directMessage": "litepub:directMessage", "listMessage": { "@id": "litepub:listMessage", diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index a5af0d1b2..64c350904 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -133,6 +133,20 @@ defmodule Pleroma.Conversation.ParticipationTest do refute participation.read end + test "it marks all the user's participations as read" do + user = insert(:user) + other_user = insert(:user) + participation1 = insert(:participation, %{read: false, user: user}) + participation2 = insert(:participation, %{read: false, user: user}) + participation3 = insert(:participation, %{read: false, user: other_user}) + + {:ok, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user) + + assert Participation.get(participation1.id).read == true + assert Participation.get(participation2.id).read == true + assert Participation.get(participation3.id).read == false + 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"}) diff --git a/test/fixtures/tesla_mock/relay@mastdon.example.org.json b/test/fixtures/tesla_mock/relay@mastdon.example.org.json new file mode 100644 index 000000000..c1fab7d3b --- /dev/null +++ b/test/fixtures/tesla_mock/relay@mastdon.example.org.json @@ -0,0 +1,55 @@ +{ + "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji" + }], + "id": "http://mastodon.example.org/users/admin", + "type": "Application", + "invisible": true, + "following": "http://mastodon.example.org/users/admin/following", + "followers": "http://mastodon.example.org/users/admin/followers", + "inbox": "http://mastodon.example.org/users/admin/inbox", + "outbox": "http://mastodon.example.org/users/admin/outbox", + "preferredUsername": "admin", + "name": null, + "summary": "\u003cp\u003e\u003c/p\u003e", + "url": "http://mastodon.example.org/@admin", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "http://mastodon.example.org/users/admin#main-key", + "owner": "http://mastodon.example.org/users/admin", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "attachment": [{ + "type": "PropertyValue", + "name": "foo", + "value": "bar" + }, + { + "type": "PropertyValue", + "name": "foo1", + "value": "bar1" + } + ], + "endpoints": { + "sharedInbox": "http://mastodon.example.org/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + }, + "image": { + "type": "Image", + "mediaType": "image/png", + "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } +} diff --git a/test/marker_test.exs b/test/marker_test.exs new file mode 100644 index 000000000..04bd67fe6 --- /dev/null +++ b/test/marker_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MarkerTest do + use Pleroma.DataCase + alias Pleroma.Marker + + import Pleroma.Factory + + describe "get_markers/2" do + test "returns user markers" do + user = insert(:user) + marker = insert(:marker, user: user) + insert(:marker, timeline: "home", user: user) + assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)] + end + end + + describe "upsert/2" do + test "creates a marker" do + user = insert(:user) + + {:ok, %{"notifications" => %Marker{} = marker}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "34"}} + ) + + assert marker.timeline == "notifications" + assert marker.last_read_id == "34" + assert marker.lock_version == 0 + end + + test "updates exist marker" do + user = insert(:user) + marker = insert(:marker, user: user, last_read_id: "8909") + + {:ok, %{"notifications" => %Marker{}}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "9909"}} + ) + + marker = refresh_record(marker) + assert marker.timeline == "notifications" + assert marker.last_read_id == "9909" + assert marker.lock_version == 0 + end + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index 54c0f9877..96316f8dd 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -683,7 +683,7 @@ defmodule Pleroma.NotificationTest do assert Notification.for_user(user) == [] end - test "it returns notifications for muted user with notifications and with_muted parameter" do + test "it returns notifications from a muted user when with_muted is set" do user = insert(:user) muted = insert(:user) {:ok, user} = User.mute(user, muted) @@ -693,27 +693,27 @@ defmodule Pleroma.NotificationTest do assert length(Notification.for_user(user, %{with_muted: true})) == 1 end - test "it returns notifications for blocked user and with_muted parameter" do + test "it doesn't return notifications from a blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user) {:ok, user} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 + assert length(Notification.for_user(user, %{with_muted: true})) == 0 end - test "it returns notificatitons for blocked domain and with_muted parameter" do + test "it doesn't return notifications from a domain-blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 + assert length(Notification.for_user(user, %{with_muted: true})) == 0 end - test "it returns notifications for muted thread with_muted parameter" do + test "it returns notifications from muted threads when with_muted is set" do user = insert(:user) another_user = insert(:user) diff --git a/test/support/factory.ex b/test/support/factory.ex index 0fdb1e952..41e2b8004 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -377,4 +377,13 @@ defmodule Pleroma.Factory do ) } end + + def marker_factory do + %Pleroma.Marker{ + user: build(:user), + timeline: "notifications", + lock_version: 0, + last_read_id: "1" + } + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 7d65209fb..eba22c40b 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -348,6 +348,14 @@ defmodule HttpRequestMock do }} end + def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json") + }} + end + def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do {:error, :nxdomain} end diff --git a/test/user_test.exs b/test/user_test.exs index ad050b7da..05bdb9a61 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1232,6 +1232,20 @@ defmodule Pleroma.UserTest do end end + describe "invisible?/1" do + test "returns true for an invisible user" do + user = insert(:user, local: true, info: %{invisible: true}) + + assert User.invisible?(user) + end + + test "returns false for a non-invisible user" do + user = insert(:user, local: true) + + refute User.invisible?(user) + end + end + describe "visible_for?/2" do test "returns true when the account is itself" do user = insert(:user, local: true) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 28a9b773c..8ae946969 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -179,6 +179,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert user.follower_address == "http://mastodon.example.org/users/admin/followers" end + test "it returns a user that is invisible" do + user_id = "http://mastodon.example.org/users/relay" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert User.invisible?(user) + end + test "it fetches the appropriate tag-restricted posts" do user = insert(:user) diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index 4a0a03944..ac2007b2c 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay @@ -19,6 +20,11 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" end + test "relay actor is invisible" do + user = Relay.get_actor() + assert User.invisible?(user) + end + describe "follow/1" do test "returns errors when user not found" do assert capture_log(fn -> diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 3155749aa..a31b4c92e 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -76,6 +76,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do assert result["image"]["url"] == "https://somebanner" end + test "renders an invisible user with the invisible property set to true" do + user = insert(:user, %{info: %{invisible: true}}) + + assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) + end + describe "endpoints" do test "local users have a usable endpoints structure" do user = insert(:user) diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs new file mode 100644 index 000000000..1fcad873d --- /dev/null +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/markers" do + test "gets markers with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69420"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0 + } + } + end + + test "gets markers with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response(403) + + assert response == %{"error" => "Insufficient permissions: read:statuses."} + end + end + + describe "POST /api/v1/markers" do + test "creates a marker with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response(200) + + assert %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => _, + "version" => 0 + } + } = response + end + + test "updates exist marker", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69477"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69888"} + }) + |> json_response(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69888", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0 + } + } + end + + test "creates a marker with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response(403) + + assert response == %{"error" => "Insufficient permissions: write:statuses."} + end + end +end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 2de2725e0..4da610b28 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -12,12 +12,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ScheduledActivity + alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI import Pleroma.Factory + clear_config([:instance, :federating]) + clear_config([:instance, :allow_relay]) + describe "posting statuses" do setup do user = insert(:user) @@ -29,6 +33,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do [conn: conn] end + test "posting a status does not increment reblog_count when relaying", %{conn: conn} do + Pleroma.Config.put([:instance, :federating], true) + Pleroma.Config.get([:instance, :allow_relay], true) + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("api/v1/statuses", %{ + "content_type" => "text/plain", + "source" => "Pleroma FE", + "status" => "Hello world", + "visibility" => "public" + }) + |> json_response(200) + + assert response["reblogs_count"] == 0 + ObanHelpers.perform_all() + + response = + conn + |> assign(:user, user) + |> get("api/v1/statuses/#{response["id"]}", %{}) + |> json_response(200) + + assert response["reblogs_count"] == 0 + end + test "posting a status", %{conn: conn} do idempotency_key = "Pikachu rocks!" diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs new file mode 100644 index 000000000..8a5c89d56 --- /dev/null +++ b/test/web/mastodon_api/views/marker_view_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do + use Pleroma.DataCase + alias Pleroma.Web.MastodonAPI.MarkerView + import Pleroma.Factory + + test "returns markers" do + marker1 = insert(:marker, timeline: "notifications", last_read_id: "17") + marker2 = insert(:marker, timeline: "home", last_read_id: "42") + + assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ + "home" => %{ + last_read_id: "42", + updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), + version: 0 + }, + "notifications" => %{ + last_read_id: "17", + updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), + version: 0 + } + } + end +end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 58534396e..37b7b62f5 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -5,7 +5,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do use Pleroma.Web.ConnCase - import ExUnit.CaptureLog import Pleroma.Factory alias Pleroma.Object diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 8a6528cbb..9cccc8c8a 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -95,6 +95,33 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do assert other_user in participation.recipients end + test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"}) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"}) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == false + assert Participation.get(participation1.id).read == false + assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 2 + + [%{"unread" => false}, %{"unread" => false}] = + conn + |> assign(:user, other_user) + |> post("/api/v1/pleroma/conversations/read", %{}) + |> json_response(200) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == true + assert Participation.get(participation1.id).read == true + assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 0 + end + describe "POST /api/v1/pleroma/notifications/read" do test "it marks a single notification as read", %{conn: conn} do user1 = insert(:user) |