diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 844f5888e..f296f7bd9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -79,7 +79,6 @@ unit-testing:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
- retry: 2
cache: &testing_cache_policy
<<: *global_cache_policy
policy: pull
@@ -117,7 +116,6 @@ unit-testing-rum:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
- retry: 2
cache: *testing_cache_policy
services:
- name: minibikini/postgres-with-rum:12
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecefba381..ee9e04568 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
+- Handle Reject for already-accepted Follows properly
+- Display OpenGraph data on alternative notice routes.
### Removed
@@ -65,6 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Attachment dimensions and blurhashes are federated when available.
- Mastodon API: support `poll` notification.
- Pinned posts federation
+- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis
### Fixed
- Don't crash so hard when email settings are invalid.
diff --git a/Dockerfile b/Dockerfile
index db1a6b457..c51ebbab0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev &&\
mkdir release &&\
mix release --path release
-FROM alpine:3.11
+FROM alpine:3.14
ARG BUILD_DATE
ARG VCS_REF
@@ -31,8 +31,7 @@ LABEL maintainer="ops@pleroma.social" \
ARG HOME=/opt/pleroma
ARG DATA=/var/lib/pleroma
-RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\
- apk update &&\
+RUN apk update &&\
apk add exiftool ffmpeg imagemagick libmagic ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\
diff --git a/config/config.exs b/config/config.exs
index 681b49827..23c41eddd 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -139,6 +139,7 @@
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
+ live_view: [signing_salt: "U5ELgdEwTD3n1+D5s0rY0AMg8/y1STxZ3Zvsl3bWh+oBcGrYdil0rXqPMRd3Glcq"],
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub_server: Pleroma.PubSub,
@@ -253,7 +254,8 @@
]
],
show_reactions: true,
- password_reset_token_validity: 60 * 60 * 24
+ password_reset_token_validity: 60 * 60 * 24,
+ profile_directory: true
config :pleroma, :welcome,
direct_message: [
diff --git a/config/description.exs b/config/description.exs
index 1c8c3b4a0..517077acf 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -936,6 +936,11 @@
key: :show_reactions,
type: :boolean,
description: "Let favourites and emoji reactions be viewed through the API."
+ },
+ %{
+ key: :profile_directory,
+ type: :boolean,
+ description: "Enable profile directory."
}
]
},
diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md
index 82483fae7..f14081893 100644
--- a/docs/development/API/admin_api.md
+++ b/docs/development/API/admin_api.md
@@ -261,6 +261,46 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
}
```
+## `PATCH /api/v1/pleroma/admin/users/suggest`
+
+### Suggest a user
+
+Adds the user(s) to follower recommendations.
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+ users: [
+ {
+ // user object
+ }
+ ]
+}
+```
+
+## `PATCH /api/v1/pleroma/admin/users/unsuggest`
+
+### Unsuggest a user
+
+Removes the user(s) from follower recommendations.
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+ users: [
+ {
+ // user object
+ }
+ ]
+}
+```
+
## `GET /api/v1/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index 6c1ecb559..518aca114 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -383,12 +383,6 @@ Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer feat
- `GET /api/v1/endorsements`: Returns an empty array, `[]`
-### Profile directory
-
-*Added in Mastodon 3.0.0*
-
-- `GET /api/v1/directory`: Returns HTTP 404
-
### Featured tags
*Added in Mastodon 3.0.0*
diff --git a/docs/development/API/nodeinfo.md b/docs/development/API/nodeinfo.md
new file mode 100644
index 000000000..0f998a1e6
--- /dev/null
+++ b/docs/development/API/nodeinfo.md
@@ -0,0 +1,347 @@
+# Nodeinfo
+
+See also [the Nodeinfo standard](https://nodeinfo.diaspora.software/).
+
+## `/.well-known/nodeinfo`
+### The well-known path
+* Method: `GET`
+* Authentication: not required
+* Params: none
+* Response: JSON
+* Example response:
+```json
+{
+ "links":[
+ {
+ "href":"https://example.com/nodeinfo/2.0.json",
+ "rel":"http://nodeinfo.diaspora.software/ns/schema/2.0"
+ },
+ {
+ "href":"https://example.com/nodeinfo/2.1.json",
+ "rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"
+ }
+ ]
+}
+```
+
+## `/nodeinfo/2.0.json`
+### Nodeinfo 2.0
+* Method: `GET`
+* Authentication: not required
+* Params: none
+* Response: JSON
+* Example response:
+```json
+{
+ "metadata":{
+ "accountActivationRequired":false,
+ "features":[
+ "pleroma_api",
+ "mastodon_api",
+ "mastodon_api_streaming",
+ "polls",
+ "pleroma_explicit_addressing",
+ "shareable_emoji_packs",
+ "multifetch",
+ "pleroma:api/v1/notifications:include_types_filter",
+ "chat",
+ "shout",
+ "relay",
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages"
+ ],
+ "federation":{
+ "enabled":true,
+ "exclusions":false,
+ "mrf_hashtag":{
+ "federated_timeline_removal":[
+
+ ],
+ "reject":[
+
+ ],
+ "sensitive":[
+ "nsfw"
+ ]
+ },
+ "mrf_object_age":{
+ "actions":[
+ "delist",
+ "strip_followers"
+ ],
+ "threshold":604800
+ },
+ "mrf_policies":[
+ "ObjectAgePolicy",
+ "TagPolicy",
+ "HashtagPolicy"
+ ],
+ "quarantined_instances":[
+
+ ]
+ },
+ "fieldsLimits":{
+ "maxFields":10,
+ "maxRemoteFields":20,
+ "nameLength":512,
+ "valueLength":2048
+ },
+ "invitesEnabled":false,
+ "mailerEnabled":false,
+ "nodeDescription":"Pleroma: An efficient and flexible fediverse server",
+ "nodeName":"Example",
+ "pollLimits":{
+ "max_expiration":31536000,
+ "max_option_chars":200,
+ "max_options":20,
+ "min_expiration":0
+ },
+ "postFormats":[
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode"
+ ],
+ "private":false,
+ "restrictedNicknames":[
+ ".well-known",
+ "~",
+ "about",
+ "activities",
+ "api",
+ "auth",
+ "check_password",
+ "dev",
+ "friend-requests",
+ "inbox",
+ "internal",
+ "main",
+ "media",
+ "nodeinfo",
+ "notice",
+ "oauth",
+ "objects",
+ "ostatus_subscribe",
+ "pleroma",
+ "proxy",
+ "push",
+ "registration",
+ "relay",
+ "settings",
+ "status",
+ "tag",
+ "user-search",
+ "user_exists",
+ "users",
+ "web",
+ "verify_credentials",
+ "update_credentials",
+ "relationships",
+ "search",
+ "confirmation_resend",
+ "mfa"
+ ],
+ "skipThreadContainment":true,
+ "staffAccounts":[
+ "https://example.com/users/admin",
+ "https://example.com/users/staff"
+ ],
+ "suggestions":{
+ "enabled":false
+ },
+ "uploadLimits":{
+ "avatar":2000000,
+ "background":4000000,
+ "banner":4000000,
+ "general":16000000
+ }
+ },
+ "openRegistrations":true,
+ "protocols":[
+ "activitypub"
+ ],
+ "services":{
+ "inbound":[
+
+ ],
+ "outbound":[
+
+ ]
+ },
+ "software":{
+ "name":"pleroma",
+ "version":"2.4.1"
+ },
+ "usage":{
+ "localPosts":27,
+ "users":{
+ "activeHalfyear":129,
+ "activeMonth":70,
+ "total":235
+ }
+ },
+ "version":"2.0"
+}
+```
+
+## `/nodeinfo/2.1.json`
+### Nodeinfo 2.1
+* Method: `GET`
+* Authentication: not required
+* Params: none
+* Response: JSON
+* Example response:
+```json
+{
+ "metadata":{
+ "accountActivationRequired":false,
+ "features":[
+ "pleroma_api",
+ "mastodon_api",
+ "mastodon_api_streaming",
+ "polls",
+ "pleroma_explicit_addressing",
+ "shareable_emoji_packs",
+ "multifetch",
+ "pleroma:api/v1/notifications:include_types_filter",
+ "chat",
+ "shout",
+ "relay",
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages"
+ ],
+ "federation":{
+ "enabled":true,
+ "exclusions":false,
+ "mrf_hashtag":{
+ "federated_timeline_removal":[
+
+ ],
+ "reject":[
+
+ ],
+ "sensitive":[
+ "nsfw"
+ ]
+ },
+ "mrf_object_age":{
+ "actions":[
+ "delist",
+ "strip_followers"
+ ],
+ "threshold":604800
+ },
+ "mrf_policies":[
+ "ObjectAgePolicy",
+ "TagPolicy",
+ "HashtagPolicy"
+ ],
+ "quarantined_instances":[
+
+ ]
+ },
+ "fieldsLimits":{
+ "maxFields":10,
+ "maxRemoteFields":20,
+ "nameLength":512,
+ "valueLength":2048
+ },
+ "invitesEnabled":false,
+ "mailerEnabled":false,
+ "nodeDescription":"Pleroma: An efficient and flexible fediverse server",
+ "nodeName":"Example",
+ "pollLimits":{
+ "max_expiration":31536000,
+ "max_option_chars":200,
+ "max_options":20,
+ "min_expiration":0
+ },
+ "postFormats":[
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode"
+ ],
+ "private":false,
+ "restrictedNicknames":[
+ ".well-known",
+ "~",
+ "about",
+ "activities",
+ "api",
+ "auth",
+ "check_password",
+ "dev",
+ "friend-requests",
+ "inbox",
+ "internal",
+ "main",
+ "media",
+ "nodeinfo",
+ "notice",
+ "oauth",
+ "objects",
+ "ostatus_subscribe",
+ "pleroma",
+ "proxy",
+ "push",
+ "registration",
+ "relay",
+ "settings",
+ "status",
+ "tag",
+ "user-search",
+ "user_exists",
+ "users",
+ "web",
+ "verify_credentials",
+ "update_credentials",
+ "relationships",
+ "search",
+ "confirmation_resend",
+ "mfa"
+ ],
+ "skipThreadContainment":true,
+ "staffAccounts":[
+ "https://example.com/users/admin",
+ "https://example.com/users/staff"
+ ],
+ "suggestions":{
+ "enabled":false
+ },
+ "uploadLimits":{
+ "avatar":2000000,
+ "background":4000000,
+ "banner":4000000,
+ "general":16000000
+ }
+ },
+ "openRegistrations":true,
+ "protocols":[
+ "activitypub"
+ ],
+ "services":{
+ "inbound":[
+
+ ],
+ "outbound":[
+
+ ]
+ },
+ "software":{
+ "name":"pleroma",
+ "repository":"https://git.pleroma.social/pleroma/pleroma",
+ "version":"2.4.1"
+ },
+ "usage":{
+ "localPosts":27,
+ "users":{
+ "activeHalfyear":129,
+ "activeMonth":70,
+ "total":235
+ }
+ },
+ "version":"2.1"
+}
+```
+
diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md
index 8f6422da0..0e7367a72 100644
--- a/docs/development/API/pleroma_api.md
+++ b/docs/development/API/pleroma_api.md
@@ -159,10 +159,12 @@ See [Admin-API](admin_api.md)
"muting": false,
"muting_notifications": false,
"subscribing": true,
+ "notifying": true,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
- "endorsed": false
+ "endorsed": false,
+ "note": ""
}
```
@@ -183,10 +185,12 @@ See [Admin-API](admin_api.md)
"muting": false,
"muting_notifications": false,
"subscribing": false,
+ "notifying": false,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
- "endorsed": false
+ "endorsed": false,
+ "note": ""
}
```
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index da27a99d0..d98cb8e37 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -199,6 +199,7 @@ def run(["gen" | rest]) do
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
+ lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
@@ -217,6 +218,7 @@ def run(["gen" | rest]) do
secret: secret,
jwt_secret: jwt_secret,
signing_salt: signing_salt,
+ lv_signing_salt: lv_signing_salt,
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
db_configurable?: db_configurable?,
diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex
index 2a9addabc..0e3e1e5de 100644
--- a/lib/pleroma/ecto_enums.ex
+++ b/lib/pleroma/ecto_enums.ex
@@ -9,7 +9,8 @@
mute: 2,
reblog_mute: 3,
notification_mute: 4,
- inverse_subscription: 5
+ inverse_subscription: 5,
+ suggestion_dismiss: 6
)
defenum(Pleroma.FollowingRelationship.State,
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 95937a892..abc95d902 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -103,6 +103,7 @@ defp load_pack(pack_dir, emoji_groups) do
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
+ Logger.info("Loading emoji pack from JSON: #{pack_file}")
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
@@ -115,6 +116,7 @@ defp load_pack(pack_dir, emoji_groups) do
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
+ Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}")
load_from_file(emoji_txt, emoji_groups)
else
extensions = Config.get([:emoji, :pack_extensions])
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 993cff09b..adb51d33a 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -338,6 +338,26 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "add_suggestion",
+ "subject" => users
+ }
+ }) do
+ "@#{actor_nickname} added suggested users: #{users_to_nicknames_string(users)}"
+ end
+
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "remove_suggestion",
+ "subject" => users
+ }
+ }) do
+ "@#{actor_nickname} removed suggested users: #{users_to_nicknames_string(users)}"
+ end
+
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 8e40dfc0d..390de1e2d 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -148,6 +148,8 @@ defmodule Pleroma.User do
field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{})
+ field(:is_suggested, :boolean, default: false)
+ field(:last_status_at, :naive_datetime)
embeds_one(
:notification_settings,
@@ -1676,6 +1678,22 @@ def confirm(%User{is_confirmed: false} = user) do
def confirm(%User{} = user), do: {:ok, user}
+ def set_suggestion(users, is_suggested) when is_list(users) do
+ Repo.transaction(fn ->
+ Enum.map(users, fn user ->
+ with {:ok, user} <- set_suggestion(user, is_suggested), do: user
+ end)
+ end)
+ end
+
+ def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
+
+ def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
+ user
+ |> change(is_suggested: is_suggested)
+ |> update_and_set_cache()
+ end
+
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
@@ -2482,4 +2500,16 @@ def active_user_count(days \\ 30) do
|> where([u], u.local == true)
|> Repo.aggregate(:count)
end
+
+ def update_last_status_at(user) do
+ User
+ |> where(id: ^user.id)
+ |> update([u], set: [last_status_at: fragment("NOW()")])
+ |> select([u], u)
+ |> Repo.update_all([])
+ |> case do
+ {1, [user]} -> set_cache(user)
+ _ -> {:error, user}
+ end
+ end
end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index ac807fc79..bf78cb32d 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -46,6 +46,8 @@ defmodule Pleroma.User.Query do
unconfirmed: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
+ is_suggested: boolean(),
+ is_discoverable: boolean(),
super_users: boolean(),
invisible: boolean(),
internal: boolean(),
@@ -167,6 +169,14 @@ defp compose_query({:unconfirmed, _}, query) do
where(query, [u], u.is_confirmed == false)
end
+ defp compose_query({:is_suggested, bool}, query) do
+ where(query, [u], u.is_suggested == ^bool)
+ end
+
+ defp compose_query({:is_discoverable, bool}, query) do
+ where(query, [u], u.is_discoverable == ^bool)
+ end
+
defp compose_query({:followers, %User{id: id}}, query) do
query
|> where([u], u.id != ^id)
diff --git a/lib/pleroma/user_note.ex b/lib/pleroma/user_note.ex
new file mode 100644
index 000000000..5e82d359f
--- /dev/null
+++ b/lib/pleroma/user_note.ex
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserNote do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.UserNote
+
+ schema "user_notes" do
+ belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
+ belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
+ field(:comment, :string)
+
+ timestamps()
+ end
+
+ def changeset(%UserNote{} = user_note, params \\ %{}) do
+ user_note
+ |> cast(params, [:source_id, :target_id, :comment])
+ |> validate_required([:source_id, :target_id])
+ end
+
+ def show(%User{} = source, %User{} = target) do
+ with %UserNote{} = note <-
+ UserNote
+ |> where(source_id: ^source.id, target_id: ^target.id)
+ |> Repo.one() do
+ note.comment
+ else
+ _ -> ""
+ end
+ end
+
+ def create(%User{} = source, %User{} = target, comment) do
+ %UserNote{}
+ |> changeset(%{
+ source_id: source.id,
+ target_id: target.id,
+ comment: comment
+ })
+ |> Repo.insert(
+ on_conflict: {:replace, [:comment]},
+ conflict_target: [:source_id, :target_id]
+ )
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 8324ca22c..756096952 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -81,6 +81,10 @@ def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
+ def update_last_status_at_if_public(actor, object) do
+ if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
+ end
+
defp increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
@@ -288,6 +292,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
_ <- increase_replies_count_if_reply(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
+ {:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity),
:ok <- maybe_federate(activity) do
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 701181a14..39d37fbcb 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -199,8 +199,9 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
+ {:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
- if in_reply_to = object.data["inReplyTo"] && object.data["type"] != "Answer" do
+ if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
Object.increase_replies_count(in_reply_to)
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 1df53f79a..c1f6b2b49 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -446,7 +446,7 @@ def update_follow_state_for_all(
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object)
- |> where(fragment("data->>'state' = 'pending'"))
+ |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex
index 637a0e702..50208a8b7 100644
--- a/lib/pleroma/web/admin_api/controllers/user_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex
@@ -35,7 +35,9 @@ defmodule Pleroma.Web.AdminAPI.UserController do
:toggle_activation,
:activate,
:deactivate,
- :approve
+ :approve,
+ :suggest,
+ :unsuggest
]
)
@@ -239,6 +241,32 @@ def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = c
render(conn, "index.json", users: updated_users)
end
+ def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.set_suggestion(users, true)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "add_suggestion"
+ })
+
+ render(conn, "index.json", users: updated_users)
+ end
+
+ def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.set_suggestion(users, false)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "remove_suggestion"
+ })
+
+ render(conn, "index.json", users: updated_users)
+ end
+
def index(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters])
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index fae0c07f0..2f1f7e627 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -80,6 +80,7 @@ def render("show.json", %{user: user}) do
"tags" => user.tags || [],
"is_confirmed" => user.is_confirmed,
"is_approved" => user.is_approved,
+ "is_suggested" => user.is_suggested,
"url" => user.uri || user.ap_id,
"registration_reason" => user.registration_reason,
"actor_type" => user.actor_type,
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 54e5ebc76..8cd2e824d 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -226,6 +226,12 @@ def follow_operation do
type: :boolean,
description: "Receive this account's reblogs in home timeline? Defaults to true.",
default: true
+ },
+ notify: %Schema{
+ type: :boolean,
+ description:
+ "Receive notifications for all statuses posted by the account? Defaults to false.",
+ default: false
}
}
},
@@ -328,6 +334,29 @@ def unblock_operation do
}
end
+ def note_operation do
+ %Operation{
+ tags: ["Account actions"],
+ summary: "Set a private note about a user.",
+ operationId: "AccountController.note",
+ security: [%{"oAuth" => ["follow", "write:accounts"]}],
+ requestBody: request_body("Parameters", note_request()),
+ description: "Create a note for the given account.",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(
+ :comment,
+ :query,
+ %Schema{type: :string},
+ "Account note body"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
def follow_by_uri_operation do
%Operation{
tags: ["Account actions"],
@@ -685,9 +714,11 @@ defp array_of_relationships do
"blocked_by" => true,
"muting" => false,
"muting_notifications" => false,
+ "note" => "",
"requested" => false,
"domain_blocking" => false,
"subscribing" => false,
+ "notifying" => false,
"endorsed" => true
},
%{
@@ -699,9 +730,11 @@ defp array_of_relationships do
"blocked_by" => true,
"muting" => true,
"muting_notifications" => false,
+ "note" => "",
"requested" => true,
"domain_blocking" => false,
"subscribing" => false,
+ "notifying" => false,
"endorsed" => false
},
%{
@@ -713,9 +746,11 @@ defp array_of_relationships do
"blocked_by" => false,
"muting" => true,
"muting_notifications" => false,
+ "note" => "",
"requested" => false,
"domain_blocking" => true,
"subscribing" => true,
+ "notifying" => true,
"endorsed" => false
}
]
@@ -760,6 +795,23 @@ defp mute_request do
}
end
+ defp note_request do
+ %Schema{
+ title: "AccountNoteRequest",
+ description: "POST body for adding a note for an account",
+ type: :object,
+ properties: %{
+ comment: %Schema{
+ type: :string,
+ description: "Account note body"
+ }
+ },
+ example: %{
+ "comment" => "Example note"
+ }
+ }
+ end
+
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex
index c9d0bfd7c..57fb1ad65 100644
--- a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex
@@ -216,7 +216,71 @@ def approve_operation do
request_body(
"Parameters",
%Schema{
- description: "POST body for deleting multiple users",
+ description: "POST body for approving multiple users",
+ type: :object,
+ properties: %{
+ nicknames: %Schema{
+ type: :array,
+ items: %Schema{type: :string}
+ }
+ }
+ }
+ ),
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :object,
+ properties: %{user: %Schema{type: :array, items: user()}}
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def suggest_operation do
+ %Operation{
+ tags: ["User administration"],
+ summary: "Suggest multiple users",
+ operationId: "AdminAPI.UserController.suggest",
+ security: [%{"oAuth" => ["admin:write:accounts"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for adding multiple suggested users",
+ type: :object,
+ properties: %{
+ nicknames: %Schema{
+ type: :array,
+ items: %Schema{type: :string}
+ }
+ }
+ }
+ ),
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :object,
+ properties: %{user: %Schema{type: :array, items: user()}}
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unsuggest_operation do
+ %Operation{
+ tags: ["User administration"],
+ summary: "Unsuggest multiple users",
+ operationId: "AdminAPI.UserController.unsuggest",
+ security: [%{"oAuth" => ["admin:write:accounts"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for removing multiple suggested users",
type: :object,
properties: %{
nicknames: %Schema{
diff --git a/lib/pleroma/web/api_spec/operations/directory_operation.ex b/lib/pleroma/web/api_spec/operations/directory_operation.ex
new file mode 100644
index 000000000..9be965feb
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/directory_operation.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
+ alias OpenApiSpex.Operation
+ alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Directory"],
+ summary: "Profile directory",
+ operationId: "DirectoryController.index",
+ parameters:
+ [
+ Operation.parameter(
+ :order,
+ :query,
+ :string,
+ "Order by recent activity or account creation",
+ required: nil
+ ),
+ Operation.parameter(:local, :query, BooleanLike, "Include local users only")
+ ] ++ pagination_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Accounts", "application/json", AccountOperation.array_of_accounts()),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
index ebcfd3be2..2a701066d 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -191,6 +191,7 @@ def delete_account_operation do
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
+ requestBody: request_body("Parameters", delete_account_request(), required: false),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
@@ -237,4 +238,48 @@ def remote_subscribe_operation do
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
}
end
+
+ def remote_interaction_operation do
+ %Operation{
+ tags: ["Accounts"],
+ summary: "Remote interaction",
+ operationId: "UtilController.remote_interaction",
+ requestBody: request_body("Parameters", remote_interaction_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ defp remote_interaction_request do
+ %Schema{
+ title: "RemoteInteractionRequest",
+ description: "POST body for remote interaction",
+ type: :object,
+ required: [:ap_id, :profile],
+ properties: %{
+ ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
+ profile: %Schema{type: :string, description: "Remote profile webfinger"}
+ }
+ }
+ end
+
+ defp delete_account_request do
+ %Schema{
+ title: "AccountDeleteRequest",
+ description: "POST body for deleting one's own account",
+ type: :object,
+ properties: %{
+ password: %Schema{
+ type: :string,
+ description: "The user's own password for confirmation.",
+ format: :password
+ }
+ },
+ example: %{
+ "password" => "prettyp0ony1313"
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index bd7143ab9..548e70544 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -194,9 +194,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
+ "note" => "",
"requested" => false,
"showing_reblogs" => true,
- "subscribing" => false
+ "subscribing" => false,
+ "notifying" => false
},
"settings_store" => %{
"pleroma-fe" => %{}
diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
index 16b73ebb4..5d9e3b56e 100644
--- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex
+++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
@@ -22,9 +22,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
id: FlakeID,
muting: %Schema{type: :boolean},
muting_notifications: %Schema{type: :boolean},
+ note: %Schema{type: :string},
requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean},
- subscribing: %Schema{type: :boolean}
+ subscribing: %Schema{type: :boolean},
+ notifying: %Schema{type: :boolean}
},
example: %{
"blocked_by" => false,
@@ -36,9 +38,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
+ "note" => "",
"requested" => false,
"showing_reblogs" => true,
- "subscribing" => false
+ "subscribing" => false,
+ "notifying" => false
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 3d042dc19..3caab0f00 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -282,9 +282,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"id" => "9toJCsKN7SmSf3aj5c",
"muting" => false,
"muting_notifications" => false,
+ "note" => "",
"requested" => false,
"showing_reblogs" => true,
- "subscribing" => false
+ "subscribing" => false,
+ "notifying" => false
},
"skip_thread_containment" => false,
"tags" => []
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 8e274de88..75484fac5 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config
socket("/socket", Pleroma.Web.UserSocket)
+ socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
diff --git a/lib/pleroma/web/manifest_controller.ex b/lib/pleroma/web/manifest_controller.ex
new file mode 100644
index 000000000..52589540b
--- /dev/null
+++ b/lib/pleroma/web/manifest_controller.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ManifestController do
+ use Pleroma.Web, :controller
+
+ plug(:skip_auth when action == :show)
+
+ @doc "GET /manifest.json"
+ def show(conn, _params) do
+ render(conn, "manifest.json")
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 5fcbffc34..5dfbecf5a 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Maps
alias Pleroma.User
+ alias Pleroma.UserNote
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@@ -53,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
when action in [:verify_credentials, :endorsements, :identity_proofs]
)
- plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"]}
+ when action in [:update_credentials, :note]
+ )
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
@@ -79,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow]
- @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+ @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a
plug(
RateLimiter,
@@ -435,6 +440,16 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
end
end
+ @doc "POST /api/v1/accounts/:id/note"
+ def note(
+ %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
+ _params
+ ) do
+ with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
+ render(conn, "relationship.json", user: noter, target: target)
+ end
+ end
+
@doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
new file mode 100644
index 000000000..45ef227fb
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
@@ -0,0 +1,82 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.DirectoryController do
+ use Pleroma.Web, :controller
+
+ import Ecto.Query
+ alias Pleroma.Pagination
+ alias Pleroma.User
+ alias Pleroma.UserRelationship
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(:skip_auth when action == "index")
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
+
+ @doc "GET /api/v1/directory"
+ def index(%{assigns: %{user: user}} = conn, params) do
+ with true <- Pleroma.Config.get([:instance, :profile_directory]) do
+ limit = Map.get(params, :limit, 20) |> min(80)
+
+ users =
+ User.Query.build(%{is_discoverable: true, invisible: false, limit: limit})
+ |> order_by_creation_date(params)
+ |> exclude_remote(params)
+ |> exclude_user(user)
+ |> exclude_relationships(user, [:block, :mute])
+ |> Pagination.fetch_paginated(params, :offset)
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ else
+ _ -> json(conn, [])
+ end
+ end
+
+ defp order_by_creation_date(query, %{order: "new"}) do
+ query
+ end
+
+ defp order_by_creation_date(query, _params) do
+ query
+ |> order_by([u], desc_nulls_last: u.last_status_at)
+ end
+
+ defp exclude_remote(query, %{local: true}) do
+ where(query, [u], u.local == true)
+ end
+
+ defp exclude_remote(query, _params) do
+ query
+ end
+
+ defp exclude_user(query, %User{id: user_id}) do
+ where(query, [u], u.id != ^user_id)
+ end
+
+ defp exclude_user(query, _user) do
+ query
+ end
+
+ defp exclude_relationships(query, %User{id: user_id}, relationship_types) do
+ query
+ |> join(:left, [u], r in UserRelationship,
+ as: :user_relationships,
+ on:
+ r.target_id == u.id and r.source_id == ^user_id and
+ r.relationship_type in ^relationship_types
+ )
+ |> where([user_relationships: r], is_nil(r.target_id))
+ end
+
+ defp exclude_relationships(query, _user, _relationship_types) do
+ query
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 64b177eb3..1459fc492 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger
+ @search_limit 40
+
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
@@ -77,7 +79,7 @@ defp search_options(params, user) do
[
resolve: params[:resolve],
following: params[:following],
- limit: params[:limit],
+ limit: min(params[:limit], @search_limit),
offset: params[:offset],
type: params[:type],
author: get_author(params),
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index 01e122dd9..e913fcf4b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -4,11 +4,16 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
+ import Ecto.Query
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.User
+ alias Pleroma.UserRelationship
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:index, :index2])
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"]} when action in [:dismiss])
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -26,7 +31,90 @@ def index_operation do
}
end
+ def index2_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Follow suggestions",
+ operationId: "SuggestionController.index2",
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
+ }
+ }
+ end
+
+ def dismiss_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Remove a suggestion",
+ operationId: "SuggestionController.dismiss",
+ parameters: [
+ OpenApiSpex.Operation.parameter(
+ :account_id,
+ :path,
+ %OpenApiSpex.Schema{type: :string},
+ "Account to dismiss",
+ required: true
+ )
+ ],
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_object_response()
+ }
+ }
+ end
+
@doc "GET /api/v1/suggestions"
def index(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
+
+ @doc "GET /api/v2/suggestions"
+ def index2(%{assigns: %{user: user}} = conn, params) do
+ limit = Map.get(params, :limit, 40) |> min(80)
+
+ users =
+ %{is_suggested: true, invisible: false, limit: limit}
+ |> User.Query.build()
+ |> exclude_user(user)
+ |> exclude_relationships(user, [:block, :mute, :suggestion_dismiss])
+ |> exclude_following(user)
+ |> Pleroma.Repo.all()
+
+ render(conn, "index.json", %{
+ users: users,
+ source: :staff,
+ for: user,
+ skip_visibility_check: true
+ })
+ end
+
+ defp exclude_user(query, %User{id: user_id}) do
+ where(query, [u], u.id != ^user_id)
+ end
+
+ defp exclude_relationships(query, %User{id: user_id}, relationship_types) do
+ query
+ |> join(:left, [u], r in UserRelationship,
+ as: :user_relationships,
+ on:
+ r.target_id == u.id and r.source_id == ^user_id and
+ r.relationship_type in ^relationship_types
+ )
+ |> where([user_relationships: r], is_nil(r.target_id))
+ end
+
+ defp exclude_following(query, %User{id: user_id}) do
+ query
+ |> join(:left, [u], r in FollowingRelationship,
+ as: :following_relationships,
+ on: r.following_id == u.id and r.follower_id == ^user_id and r.state == :follow_accept
+ )
+ |> where([following_relationships: r], is_nil(r.following_id))
+ end
+
+ @doc "DELETE /api/v1/suggestions/:account_id"
+ def dismiss(%{assigns: %{user: source}} = conn, %{account_id: user_id}) do
+ with %User{} = target <- User.get_cached_by_id(user_id),
+ {:ok, _} <- UserRelationship.create(:suggestion_dismiss, source, target) do
+ json(conn, %{})
+ end
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 71479550e..23846b36a 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -24,6 +24,7 @@ def follow(follower, followed, params \\ %{}) do
with {:ok, follower, _followed, _} <- result do
options = cast_params(params)
set_reblogs_visibility(options[:reblogs], result)
+ set_subscription(options[:notify], result)
{:ok, follower}
end
end
@@ -36,6 +37,16 @@ defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
CommonAPI.show_reblogs(follower, followed)
end
+ defp set_subscription(true, {:ok, follower, followed, _}) do
+ User.subscribe(follower, followed)
+ end
+
+ defp set_subscription(false, {:ok, follower, followed, _}) do
+ User.unsubscribe(follower, followed)
+ end
+
+ defp set_subscription(_, _), do: {:ok, nil}
+
@spec get_followers(User.t(), map()) :: list(User.t())
def get_followers(user, params \\ %{}) do
user
@@ -73,7 +84,8 @@ defp cast_params(params) do
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
- account_ap_id: :string
+ account_ap_id: :string,
+ notify: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 9e9de33f6..4b15b1635 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.FollowingRelationship
alias Pleroma.User
+ alias Pleroma.UserNote
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
@@ -101,6 +102,15 @@ def render(
User.following?(target, reading_user)
end
+ subscribing =
+ UserRelationship.exists?(
+ user_relationships,
+ :inverse_subscription,
+ target,
+ reading_user,
+ &User.subscribed_to?(&2, &1)
+ )
+
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
@@ -138,14 +148,8 @@ def render(
target,
&User.muted_notifications?(&1, &2)
),
- subscribing:
- UserRelationship.exists?(
- user_relationships,
- :inverse_subscription,
- target,
- reading_user,
- &User.subscribed_to?(&2, &1)
- ),
+ subscribing: subscribing,
+ notifying: subscribing,
requested: follow_state == :follow_pending,
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
@@ -156,7 +160,12 @@ def render(
target,
&User.muting_reblogs?(&1, &2)
),
- endorsed: false
+ endorsed: false,
+ note:
+ UserNote.show(
+ reading_user,
+ target
+ )
}
end
@@ -261,6 +270,7 @@ defp do_render("show.json", %{user: user} = opts) do
actor_type: user.actor_type
}
},
+ last_status_at: user.last_status_at,
# Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
@@ -269,6 +279,7 @@ defp do_render("show.json", %{user: user} = opts) do
ap_id: user.ap_id,
also_known_as: user.also_known_as,
is_confirmed: user.is_confirmed,
+ is_suggested: user.is_suggested,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
hide_follows_count: user.hide_follows_count,
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index ef208062b..7072d5d61 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -59,6 +59,7 @@ def features do
"mastodon_api",
"mastodon_api_streaming",
"polls",
+ "v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
@@ -83,7 +84,13 @@ def features do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions",
- "pleroma_chat_messages"
+ "pleroma_chat_messages",
+ if Config.get([:instance, :show_reactions]) do
+ "exposable_reactions"
+ end,
+ if Config.get([:instance, :profile_directory]) do
+ "profile_directory"
+ end
]
|> Enum.filter(& &1)
end
diff --git a/lib/pleroma/web/mastodon_api/views/suggestion_view.ex b/lib/pleroma/web/mastodon_api/views/suggestion_view.ex
new file mode 100644
index 000000000..865229a88
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/suggestion_view.ex
@@ -0,0 +1,28 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.SuggestionView do
+ use Pleroma.Web, :view
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ @source_types [:staff, :global, :past_interactions]
+
+ def render("index.json", %{users: users} = opts) do
+ Enum.map(users, fn user ->
+ opts =
+ opts
+ |> Map.put(:user, user)
+ |> Map.delete(:users)
+
+ render("show.json", opts)
+ end)
+ end
+
+ def render("show.json", %{source: source, user: _user} = opts) when source in @source_types do
+ %{
+ source: source,
+ account: AccountView.render("show.json", opts)
+ }
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
index dcd54b1af..669d50132 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -151,7 +151,9 @@ def index2(%{assigns: %{user: user}} = conn, params) do
index_query(user, params)
|> Pagination.fetch_paginated(params)
- render(conn, "index.json", chats: chats)
+ conn
+ |> add_link_headers(chats)
+ |> render("index.json", chats: chats)
end
defp index_query(%{id: user_id} = user, params) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index dae113617..b2ca09784 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
+ import Phoenix.LiveDashboard.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
@@ -150,6 +151,7 @@ defmodule Pleroma.Web.Router do
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
+ post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
@@ -157,12 +159,11 @@ defmodule Pleroma.Web.Router do
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
+ # AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
- put("/users/tag", AdminAPIController, :tag_users)
- delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
@@ -185,16 +186,61 @@ defmodule Pleroma.Web.Router do
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
- delete("/users", UserController, :delete)
post("/users", UserController, :create)
+
+ patch("/users/suggest", UserController, :suggest)
+ patch("/users/unsuggest", UserController, :unsuggest)
+
+ get("/relay", RelayController, :index)
+ post("/relay", RelayController, :follow)
+ delete("/relay", RelayController, :unfollow)
+
+ get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+ patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
+ get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
+ patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
+
+ get("/instance_document/:name", InstanceDocumentController, :show)
+ patch("/instance_document/:name", InstanceDocumentController, :update)
+ delete("/instance_document/:name", InstanceDocumentController, :delete)
+
+ patch("/users/confirm_email", AdminAPIController, :confirm_email)
+ patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
+
+ get("/config", ConfigController, :show)
+ post("/config", ConfigController, :update)
+ get("/config/descriptions", ConfigController, :descriptions)
+ get("/need_reboot", AdminAPIController, :need_reboot)
+ get("/restart", AdminAPIController, :restart)
+
+ get("/oauth_app", OAuthAppController, :index)
+ post("/oauth_app", OAuthAppController, :create)
+ patch("/oauth_app/:id", OAuthAppController, :update)
+ delete("/oauth_app/:id", OAuthAppController, :delete)
+
+ get("/media_proxy_caches", MediaProxyCacheController, :index)
+ post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
+ post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
+
+ get("/frontends", FrontendController, :index)
+ post("/frontends/install", FrontendController, :install)
+
+ post("/backups", AdminAPIController, :create_backup)
+ end
+
+ # AdminAPI: admins and mods (staff) can perform these actions
+ scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
+ pipe_through(:admin_api)
+
+ put("/users/tag", AdminAPIController, :tag_users)
+ delete("/users/tag", AdminAPIController, :untag_users)
+
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve)
- get("/relay", RelayController, :index)
- post("/relay", RelayController, :follow)
- delete("/relay", RelayController, :unfollow)
+ delete("/users", UserController, :delete)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
@@ -214,13 +260,6 @@ defmodule Pleroma.Web.Router do
get("/instances/:instance/statuses", InstanceController, :list_statuses)
delete("/instances/:instance", InstanceController, :delete)
- get("/instance_document/:name", InstanceDocumentController, :show)
- patch("/instance_document/:name", InstanceDocumentController, :update)
- delete("/instance_document/:name", InstanceDocumentController, :delete)
-
- patch("/users/confirm_email", AdminAPIController, :confirm_email)
- patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
-
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
@@ -232,39 +271,19 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id", StatusController, :delete)
get("/statuses", StatusController, :index)
- get("/config", ConfigController, :show)
- post("/config", ConfigController, :update)
- get("/config/descriptions", ConfigController, :descriptions)
- get("/need_reboot", AdminAPIController, :need_reboot)
- get("/restart", AdminAPIController, :restart)
-
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
- get("/oauth_app", OAuthAppController, :index)
- post("/oauth_app", OAuthAppController, :create)
- patch("/oauth_app/:id", OAuthAppController, :update)
- delete("/oauth_app/:id", OAuthAppController, :delete)
-
- get("/media_proxy_caches", MediaProxyCacheController, :index)
- post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
- post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
-
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
-
- get("/frontends", FrontendController, :index)
- post("/frontends/install", FrontendController, :install)
-
- post("/backups", AdminAPIController, :create_backup)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
- pipe_through([:admin_api, :require_admin])
+ pipe_through(:admin_api)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
@@ -279,7 +298,7 @@ defmodule Pleroma.Web.Router do
# Modifying packs
scope "/packs" do
- pipe_through([:admin_api, :require_admin])
+ pipe_through(:admin_api)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
@@ -456,6 +475,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
+ post("/accounts/:id/note", AccountController, :note)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
@@ -535,6 +555,7 @@ defmodule Pleroma.Web.Router do
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
+ delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
@@ -579,6 +600,8 @@ defmodule Pleroma.Web.Router do
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
+
+ get("/directory", DirectoryController, :index)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -586,6 +609,8 @@ defmodule Pleroma.Web.Router do
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
+
+ get("/suggestions", SuggestionController, :index2)
end
scope "/api", Pleroma.Web do
@@ -627,6 +652,11 @@ defmodule Pleroma.Web.Router do
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
+ # Notice compatibility routes for other frontends
+ get("/@:nickname/:id", OStatus.OStatusController, :notice)
+ get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
+ get("/:nickname/status/:id", OStatus.OStatusController, :notice)
+
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
@@ -736,6 +766,12 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
+ scope "/", Pleroma.Web do
+ pipe_through(:api)
+
+ get("/manifest.json", ManifestController, :show)
+ end
+
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
@@ -757,6 +793,11 @@ defmodule Pleroma.Web.Router do
end
end
+ scope "/" do
+ pipe_through([:pleroma_html, :authenticate, :require_admin])
+ live_dashboard("/phoenix/live_dashboard")
+ end
+
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 50f0927a3..827c0a384 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -167,6 +167,15 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
+ defp assign_id(%{path_info: ["@" <> _nickname, notice_id]} = conn, _opts),
+ do: assign(conn, :notice_id, notice_id)
+
+ defp assign_id(%{path_info: ["@" <> _nickname, "posts", notice_id]} = conn, _opts),
+ do: assign(conn, :notice_id, notice_id)
+
+ defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts),
+ do: assign(conn, :notice_id, notice_id)
+
defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id)
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index ef43f7682..ccbef6d9f 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -62,6 +62,15 @@ def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profil
end
end
+ def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
+ with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
+ conn
+ |> json(%{url: String.replace(template, "{uri}", ap_id)})
+ else
+ _e -> json(conn, %{error: "Couldn't find user"})
+ end
+ end
+
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")
end
@@ -123,8 +132,10 @@ def change_email(%{assigns: %{user: user}, body_params: body_params} = conn, %{}
end
end
- def delete_account(%{assigns: %{user: user}} = conn, params) do
- password = params[:password] || ""
+ def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
+ # This endpoint can accept a query param or JSON body for backwards-compatibility.
+ # Submitting a JSON body is recommended, so passwords don't end up in server logs.
+ password = body_params[:password] || params[:password] || ""
case CommonAPI.Utils.confirm_current_password(user, password) do
{:ok, user} ->
diff --git a/lib/pleroma/web/views/manifest_view.ex b/lib/pleroma/web/views/manifest_view.ex
new file mode 100644
index 000000000..cc78ea347
--- /dev/null
+++ b/lib/pleroma/web/views/manifest_view.ex
@@ -0,0 +1,28 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ManifestView do
+ use Pleroma.Web, :view
+ alias Pleroma.Config
+ alias Pleroma.Web.Endpoint
+
+ 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: Endpoint.url(),
+ start_url: "/",
+ categories: [
+ "social"
+ ],
+ serviceworker: %{
+ src: "/sw.js"
+ }
+ }
+ end
+end
diff --git a/mix.exs b/mix.exs
index 581b07f67..360d49905 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,7 +8,7 @@ def project do
elixir: "~> 1.9",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
- elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())],
+ elixirc_options: [warnings_as_errors: warnings_as_errors()],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
@@ -79,6 +79,7 @@ def application do
:comeonin,
:quack,
:fast_sanitize,
+ :os_mon,
:ssl
],
included_applications: [:ex_syslogger]
@@ -90,8 +91,7 @@ defp elixirc_paths(:benchmark), do: ["lib", "benchmarks", "priv/scrubbers"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
- defp warnings_as_errors(:prod), do: false
- defp warnings_as_errors(_), do: true
+ defp warnings_as_errors, do: System.get_env("CI") == "true"
# Specifies OAuth dependencies.
defp oauth_deps do
@@ -129,7 +129,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 2.14"},
+ {:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.2"},
{:poison, "~> 3.0", override: true},
@@ -196,6 +196,8 @@ defp deps do
{:majic, "~> 1.0"},
{:eblurhash, "~> 1.1.0"},
{:open_api_spex, "~> 3.10"},
+ {:phoenix_live_dashboard, "~> 0.6.2"},
+ {:ecto_psql_extras, "~> 0.6"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
diff --git a/mix.lock b/mix.lock
index f9646d7ac..e9b1ed619 100644
--- a/mix.lock
+++ b/mix.lock
@@ -32,6 +32,7 @@
"eblurhash": {:hex, :eblurhash, "1.1.0", "e10ccae762598507ebfacf0b645ed49520f2afa3e7e9943e73a91117dffce415", [:rebar3], [], "hexpm", "2e6b889d09fddd374e3c5ac57c486138768763264e99ac1074ae5fa7fc9ab51d"},
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
+ "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.4", "5d43fd088d39a158c860b17e8d210669587f63ec89ea122a4654861c8c6e2db4", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.15.7", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "311db02f1b772e3d0dc7f56a05044b5e1499d78ed6abf38885e1ca70059449e5"},
"ecto_sql": {:hex, :ecto_sql, "3.6.2", "9526b5f691701a5181427634c30655ac33d11e17e4069eff3ae1176c764e0ba3", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.6.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ec9d7e6f742ea39b63aceaea9ac1d1773d574ea40df5a53ef8afbd9242fdb6b"},
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
@@ -94,7 +95,9 @@
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
"phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
- "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
+ "phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.2", "0769470265eb13af01b5001b29cb935f4710d6adaa1ffc18417a570a337a2f0f", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5bc6c6b38a2ca8b5020b442322fcee6afd5e641637a0b1fb059d4bd89bc58e7b"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.3", "039435dd975f7e55953525b88f1d596f26c6141412584c16f4db109708a8ee68", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4a540cea32e05356541737033d666ee7fea7700eb2101bf76783adbfe06601cd"},
"plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
@@ -120,7 +123,9 @@
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "1.3.11", "34f79c57f19892b43bd2168de9ff5de478a721a26328ef59567aad4243e7a77b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f1e2a048db454f9982b9cf840f75e7399dd48be31ecc2a7dc10012a803b913af"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
+ "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"tesla": {:hex, :tesla, "1.4.1", "ff855f1cac121e0d16281b49e8f066c4a0d89965f98864515713878cca849ac8", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "95f5de35922c8c4b3945bee7406f66eb680b0955232f78f5fb7e853aa1ce201a"},
"timex": {:hex, :timex, "3.7.5", "3eca56e23bfa4e0848f0b0a29a92fa20af251a975116c6d504966e8a90516dfd", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a15608dca680f2ef663d71c95842c67f0af08a0f3b1d00e17bbd22872e2874e4"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
diff --git a/priv/repo/migrations/20211121000000_create_user_notes.exs b/priv/repo/migrations/20211121000000_create_user_notes.exs
new file mode 100644
index 000000000..b75e11695
--- /dev/null
+++ b/priv/repo/migrations/20211121000000_create_user_notes.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateUserNotes do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:user_notes) do
+ add(:source_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:target_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:comment, :string)
+
+ timestamps()
+ end
+
+ create_if_not_exists(unique_index(:user_notes, [:source_id, :target_id]))
+ end
+end
diff --git a/priv/repo/migrations/20211126191138_add_suggestions.exs b/priv/repo/migrations/20211126191138_add_suggestions.exs
new file mode 100644
index 000000000..7cc67d8ef
--- /dev/null
+++ b/priv/repo/migrations/20211126191138_add_suggestions.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddSuggestions do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add(:is_suggested, :boolean, default: false, null: false)
+ end
+
+ create_if_not_exists(index(:users, [:is_suggested]))
+ end
+end
diff --git a/priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs b/priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs
new file mode 100644
index 000000000..906178216
--- /dev/null
+++ b/priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddLastStatusAtToUsers do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add(:last_status_at, :naive_datetime)
+ end
+
+ create_if_not_exists(index(:users, [:last_status_at]))
+ end
+end
diff --git a/priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs b/priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs
new file mode 100644
index 000000000..9f8f52b65
--- /dev/null
+++ b/priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddIsDiscoverableIndexToUsers do
+ use Ecto.Migration
+
+ def change do
+ create(index(:users, [:is_discoverable]))
+ end
+end
diff --git a/priv/templates/sample_config.eex b/priv/templates/sample_config.eex
index 42f496ded..0068969ac 100644
--- a/priv/templates/sample_config.eex
+++ b/priv/templates/sample_config.eex
@@ -13,6 +13,7 @@ config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
http: [ip: {<%= String.replace(listen_ip, ".", ", ") %>}, port: <%= listen_port %>],
secret_key_base: "<%= secret %>",
+ live_view: [signing_salt: "<%= lv_signing_salt %>"],
signing_salt: "<%= signing_salt %>"
config :pleroma, :instance,
diff --git a/test/pleroma/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs
index 7d51fd84c..9e3f11f1a 100644
--- a/test/pleroma/config/transfer_task_test.exs
+++ b/test/pleroma/config/transfer_task_test.exs
@@ -82,6 +82,7 @@ test "transfer config values with full subkey update" do
on_exit(fn -> Restarter.Pleroma.refresh() end)
end
+ @tag :erratic
test "don't restart if no reboot time settings were changed" do
clear_config(:emoji)
insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
@@ -92,18 +93,21 @@ test "don't restart if no reboot time settings were changed" do
)
end
+ @tag :erratic
test "on reboot time key" do
clear_config(:shout)
insert(:config, key: :shout, value: [enabled: false])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
+ @tag :erratic
test "on reboot time subkey" do
clear_config(Pleroma.Captcha)
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
+ @tag :erratic
test "don't restart pleroma on reboot time key and subkey if there is false flag" do
clear_config(:shout)
clear_config(Pleroma.Captcha)
diff --git a/test/pleroma/user/query_test.exs b/test/pleroma/user/query_test.exs
index 357016e3e..363da7665 100644
--- a/test/pleroma/user/query_test.exs
+++ b/test/pleroma/user/query_test.exs
@@ -34,4 +34,14 @@ test "it returns internal users when enabled" do
assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2
end
end
+
+ test "is_suggested param" do
+ _user1 = insert(:user, is_suggested: false)
+ user2 = insert(:user, is_suggested: true)
+
+ assert [^user2] =
+ %{is_suggested: true}
+ |> User.Query.build()
+ |> Repo.all()
+ end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 12d5d5db6..6cd93c34c 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -1718,6 +1718,38 @@ test "delete/1 purges a remote user" do
assert user.banner == %{}
end
+ describe "set_suggestion" do
+ test "suggests a user" do
+ user = insert(:user, is_suggested: false)
+ refute user.is_suggested
+ {:ok, user} = User.set_suggestion(user, true)
+ assert user.is_suggested
+ end
+
+ test "suggests a list of users" do
+ unsuggested_users = [
+ insert(:user, is_suggested: false),
+ insert(:user, is_suggested: false),
+ insert(:user, is_suggested: false)
+ ]
+
+ {:ok, users} = User.set_suggestion(unsuggested_users, true)
+
+ assert Enum.count(users) == 3
+
+ Enum.each(users, fn user ->
+ assert user.is_suggested
+ end)
+ end
+
+ test "unsuggests a user" do
+ user = insert(:user, is_suggested: true)
+ assert user.is_suggested
+ {:ok, user} = User.set_suggestion(user, false)
+ refute user.is_suggested
+ end
+ end
+
test "get_public_key_for_ap_id fetches a user that's not in the db" do
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs
index d0988619d..c6155ed18 100644
--- a/test/pleroma/web/activity_pub/side_effects_test.exs
+++ b/test/pleroma/web/activity_pub/side_effects_test.exs
@@ -88,6 +88,16 @@ test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do
assert User.blocks?(user, blocked)
end
+ test "it updates following relationship", %{user: user, blocked: blocked, block: block} do
+ {:ok, _, _} = SideEffects.handle(block)
+
+ refute Pleroma.FollowingRelationship.get(user, blocked)
+ assert User.get_follow_state(user, blocked) == nil
+ assert User.get_follow_state(blocked, user) == nil
+ assert User.get_follow_state(user, blocked, nil) == nil
+ assert User.get_follow_state(blocked, user, nil) == nil
+ end
+
test "it blocks but does not unfollow if the relevant setting is set", %{
user: user,
blocked: blocked,
@@ -542,4 +552,74 @@ test "it streams out the announce", %{announce: announce} do
end
end
end
+
+ describe "removing a follower" do
+ setup do
+ user = insert(:user)
+ followed = insert(:user)
+
+ {:ok, _, _, follow_activity} = CommonAPI.follow(user, followed)
+
+ {:ok, reject_data, []} = Builder.reject(followed, follow_activity)
+ {:ok, reject, _meta} = ActivityPub.persist(reject_data, local: true)
+
+ %{user: user, followed: followed, reject: reject}
+ end
+
+ test "", %{user: user, followed: followed, reject: reject} do
+ assert User.following?(user, followed)
+ assert Pleroma.FollowingRelationship.get(user, followed)
+
+ {:ok, _, _} = SideEffects.handle(reject)
+
+ refute User.following?(user, followed)
+ refute Pleroma.FollowingRelationship.get(user, followed)
+ assert User.get_follow_state(user, followed) == nil
+ assert User.get_follow_state(user, followed, nil) == nil
+ end
+ end
+
+ describe "removing a follower from remote" do
+ setup do
+ user = insert(:user)
+ followed = insert(:user, local: false)
+
+ # Mock a local-to-remote follow
+ {:ok, follow_data, []} = Builder.follow(user, followed)
+
+ follow_data =
+ follow_data
+ |> Map.put("state", "accept")
+
+ {:ok, follow, _meta} = ActivityPub.persist(follow_data, local: true)
+ {:ok, _, _} = SideEffects.handle(follow)
+
+ # Mock a remote-to-local accept
+ {:ok, accept_data, _} = Builder.accept(followed, follow)
+ {:ok, accept, _} = ActivityPub.persist(accept_data, local: false)
+ {:ok, _, _} = SideEffects.handle(accept)
+
+ # Mock a remote-to-local reject
+ {:ok, reject_data, []} = Builder.reject(followed, follow)
+ {:ok, reject, _meta} = ActivityPub.persist(reject_data, local: false)
+
+ %{user: user, followed: followed, reject: reject}
+ end
+
+ test "", %{user: user, followed: followed, reject: reject} do
+ assert User.following?(user, followed)
+ assert Pleroma.FollowingRelationship.get(user, followed)
+
+ {:ok, _, _} = SideEffects.handle(reject)
+
+ refute User.following?(user, followed)
+ refute Pleroma.FollowingRelationship.get(user, followed)
+
+ assert Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, followed).data["state"] ==
+ "reject"
+
+ assert User.get_follow_state(user, followed) == nil
+ assert User.get_follow_state(user, followed, nil) == nil
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index ee3e1014e..62dc02f61 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -213,6 +213,20 @@ test "updates the state of all Follow activities with the same actor and object"
assert refresh_record(follow_activity).data["state"] == "accept"
assert refresh_record(follow_activity_two).data["state"] == "accept"
end
+
+ test "also updates the state of accepted follows" do
+ user = insert(:user)
+ follower = insert(:user)
+
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
+
+ {:ok, follow_activity_two} =
+ Utils.update_follow_state_for_all(follow_activity_two, "reject")
+
+ assert refresh_record(follow_activity).data["state"] == "reject"
+ assert refresh_record(follow_activity_two).data["state"] == "reject"
+ end
end
describe "update_follow_state/2" do
diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
index d9da34f6e..b199fa704 100644
--- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
@@ -873,6 +873,56 @@ test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
"@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
end
+ test "PATCH /api/pleroma/admin/users/suggest", %{admin: admin, conn: conn} do
+ user1 = insert(:user, is_suggested: false)
+ user2 = insert(:user, is_suggested: false)
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/suggest",
+ %{nicknames: [user1.nickname, user2.nickname]}
+ )
+ |> json_response_and_validate_schema(200)
+
+ assert Enum.map(response["users"], & &1["is_suggested"]) == [true, true]
+ [user1, user2] = Repo.reload!([user1, user2])
+
+ assert user1.is_suggested
+ assert user2.is_suggested
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} added suggested users: @#{user1.nickname}, @#{user2.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/unsuggest", %{admin: admin, conn: conn} do
+ user1 = insert(:user, is_suggested: true)
+ user2 = insert(:user, is_suggested: true)
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch(
+ "/api/pleroma/admin/users/unsuggest",
+ %{nicknames: [user1.nickname, user2.nickname]}
+ )
+ |> json_response_and_validate_schema(200)
+
+ assert Enum.map(response["users"], & &1["is_suggested"]) == [false, false]
+ [user1, user2] = Repo.reload!([user1, user2])
+
+ refute user1.is_suggested
+ refute user2.is_suggested
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} removed suggested users: @#{user1.nickname}, @#{user2.nickname}"
+ end
+
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user)
@@ -906,6 +956,7 @@ defp user_response(user, attrs \\ %{}) do
"display_name" => HTML.strip_tags(user.name || user.nickname),
"is_confirmed" => true,
"is_approved" => true,
+ "is_suggested" => false,
"url" => user.ap_id,
"registration_reason" => nil,
"actor_type" => "Person",
diff --git a/test/pleroma/web/manifest_controller_test.exs b/test/pleroma/web/manifest_controller_test.exs
new file mode 100644
index 000000000..b7a4940db
--- /dev/null
+++ b/test/pleroma/web/manifest_controller_test.exs
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ManifestControllerTest do
+ use Pleroma.Web.ConnCase
+
+ setup do
+ clear_config([:instance, :name], "Manifest Test")
+ clear_config([:manifest, :theme_color], "#ff0000")
+ end
+
+ test "manifest.json", %{conn: conn} do
+ conn = get(conn, "/manifest.json")
+ assert %{"name" => "Manifest Test", "theme_color" => "#ff0000"} = json_response(conn, 200)
+ end
+end
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index a92a58224..966a4072d 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -922,6 +922,27 @@ test "following with reblogs" do
|> json_response_and_validate_schema(200)
end
+ test "following with subscription and unsubscribing" do
+ %{conn: conn} = oauth_access(["follow"])
+ followed = insert(:user)
+
+ ret_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true})
+
+ assert %{"id" => _id, "subscribing" => true} =
+ json_response_and_validate_schema(ret_conn, 200)
+
+ ret_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false})
+
+ assert %{"id" => _id, "subscribing" => false} =
+ json_response_and_validate_schema(ret_conn, 200)
+ end
+
test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
@@ -1776,4 +1797,21 @@ test "getting a list of blocks" do
assert [%{"id" => ^id2}] = result
end
+
+ test "create a note on a user" do
+ %{conn: conn} = oauth_access(["write:accounts", "read:follows"])
+ other_user = insert(:user)
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{other_user.id}/note", %{
+ "comment" => "Example note"
+ })
+
+ assert [%{"note" => "Example note"}] =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/accounts/relationships?id=#{other_user.id}")
+ |> json_response_and_validate_schema(200)
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs
new file mode 100644
index 000000000..b8f55f832
--- /dev/null
+++ b/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs
@@ -0,0 +1,46 @@
+defmodule Pleroma.Web.MastodonAPI.DirectoryControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Web.CommonAPI
+ import Pleroma.Factory
+
+ test "GET /api/v1/directory with :profile_directory disabled returns empty array", %{conn: conn} do
+ clear_config([:instance, :profile_directory], false)
+
+ insert(:user, is_discoverable: true)
+ insert(:user, is_discoverable: true)
+
+ result =
+ conn
+ |> get("/api/v1/directory")
+ |> json_response_and_validate_schema(200)
+
+ assert result == []
+ end
+
+ test "GET /api/v1/directory returns discoverable users only", %{conn: conn} do
+ %{id: user_id} = insert(:user, is_discoverable: true)
+ insert(:user, is_discoverable: false)
+
+ result =
+ conn
+ |> get("/api/v1/directory")
+ |> json_response_and_validate_schema(200)
+
+ assert [%{"id" => ^user_id}] = result
+ end
+
+ test "GET /api/v1/directory returns users sorted by most recent statuses", %{conn: conn} do
+ insert(:user, is_discoverable: true)
+ %{id: user_id} = user = insert(:user, is_discoverable: true)
+ insert(:user, is_discoverable: true)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "yay i'm discoverable"})
+
+ result =
+ conn
+ |> get("/api/v1/directory?order=active")
+ |> json_response_and_validate_schema(200)
+
+ assert [%{"id" => ^user_id} | _tail] = result
+ end
+end
diff --git a/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs
index 168966fc9..89273e67b 100644
--- a/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs
@@ -4,8 +4,11 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.UserRelationship
+ alias Pleroma.Web.CommonAPI
+ import Pleroma.Factory
- setup do: oauth_access(["read"])
+ setup do: oauth_access(["read", "write"])
test "returns empty result", %{conn: conn} do
res =
@@ -15,4 +18,66 @@ test "returns empty result", %{conn: conn} do
assert res == []
end
+
+ test "returns v2 suggestions", %{conn: conn} do
+ %{id: user_id} = insert(:user, is_suggested: true)
+
+ res =
+ conn
+ |> get("/api/v2/suggestions")
+ |> json_response_and_validate_schema(200)
+
+ assert [%{"source" => "staff", "account" => %{"id" => ^user_id}}] = res
+ end
+
+ test "returns v2 suggestions excluding dismissed accounts", %{conn: conn} do
+ %{id: user_id} = insert(:user, is_suggested: true)
+
+ conn
+ |> delete("/api/v1/suggestions/#{user_id}")
+ |> json_response_and_validate_schema(200)
+
+ res =
+ conn
+ |> get("/api/v2/suggestions")
+ |> json_response_and_validate_schema(200)
+
+ assert [] = res
+ end
+
+ test "returns v2 suggestions excluding blocked accounts", %{conn: conn, user: blocker} do
+ blocked = insert(:user, is_suggested: true)
+ {:ok, _} = CommonAPI.block(blocker, blocked)
+
+ res =
+ conn
+ |> get("/api/v2/suggestions")
+ |> json_response_and_validate_schema(200)
+
+ assert [] = res
+ end
+
+ test "returns v2 suggestions excluding followed accounts", %{conn: conn, user: follower} do
+ followed = insert(:user, is_suggested: true)
+ {:ok, _, _, _} = CommonAPI.follow(follower, followed)
+
+ res =
+ conn
+ |> get("/api/v2/suggestions")
+ |> json_response_and_validate_schema(200)
+
+ assert [] = res
+ end
+
+ test "dismiss suggestion", %{conn: conn, user: source} do
+ target = insert(:user, is_suggested: true)
+
+ res =
+ conn
+ |> delete("/api/v1/suggestions/#{target.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert res == %{}
+ assert UserRelationship.exists?(:suggestion_dismiss, source, target)
+ end
end
diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs
index 60881756d..c23ffb966 100644
--- a/test/pleroma/web/mastodon_api/views/account_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs
@@ -74,6 +74,7 @@ test "Represent a user account" do
fields: []
},
fqn: "shp@shitposter.club",
+ last_status_at: nil,
pleroma: %{
ap_id: user.ap_id,
also_known_as: ["https://shitposter.zone/users/shp"],
@@ -83,6 +84,7 @@ test "Represent a user account" do
tags: [],
is_admin: false,
is_moderator: false,
+ is_suggested: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
@@ -174,6 +176,7 @@ test "Represent a Service(bot) account" do
fields: []
},
fqn: "shp@shitposter.club",
+ last_status_at: nil,
pleroma: %{
ap_id: user.ap_id,
also_known_as: [],
@@ -183,6 +186,7 @@ test "Represent a Service(bot) account" do
tags: [],
is_admin: false,
is_moderator: false,
+ is_suggested: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
@@ -268,10 +272,12 @@ defp test_relationship_rendering(user, other_user, expected_result) do
muting: false,
muting_notifications: false,
subscribing: false,
+ notifying: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
- endorsed: false
+ endorsed: false,
+ note: ""
}
test "represent a relationship for the following and followed user" do
@@ -293,6 +299,7 @@ test "represent a relationship for the following and followed user" do
muting: true,
muting_notifications: true,
subscribing: true,
+ notifying: true,
showing_reblogs: false,
id: to_string(other_user.id)
}
diff --git a/test/pleroma/web/mastodon_api/views/suggestion_view_test.exs b/test/pleroma/web/mastodon_api/views/suggestion_view_test.exs
new file mode 100644
index 000000000..5aae36ce9
--- /dev/null
+++ b/test/pleroma/web/mastodon_api/views/suggestion_view_test.exs
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.SuggestionViewTest do
+ use Pleroma.DataCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.Web.MastodonAPI.SuggestionView, as: View
+
+ test "show.json" do
+ user = insert(:user, is_suggested: true)
+ json = View.render("show.json", %{user: user, source: :staff, skip_visibility_check: true})
+
+ assert json.source == :staff
+ assert json.account.id == user.id
+ end
+
+ test "index.json" do
+ user1 = insert(:user, is_suggested: true)
+ user2 = insert(:user, is_suggested: true)
+ user3 = insert(:user, is_suggested: true)
+
+ [suggestion1, suggestion2, suggestion3] =
+ View.render("index.json", %{
+ users: [user1, user2, user3],
+ source: :staff,
+ skip_visibility_check: true
+ })
+
+ assert suggestion1.source == :staff
+ assert suggestion2.account.id == user2.id
+ assert suggestion3.account.url == user3.ap_id
+ end
+end
diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs
index 81d669837..41aef98b1 100644
--- a/test/pleroma/web/o_status/o_status_controller_test.exs
+++ b/test/pleroma/web/o_status/o_status_controller_test.exs
@@ -343,4 +343,54 @@ test "does not require authentication on non-federating instances", %{
|> response(200)
end
end
+
+ describe "notice compatibility routes" do
+ test "Soapbox FE", %{conn: conn} do
+ user = insert(:user)
+ note_activity = insert(:note_activity, user: user)
+
+ resp =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/@#{user.nickname}/posts/#{note_activity.id}")
+ |> response(200)
+
+ expected =
+ ""
+
+ assert resp =~ expected
+ end
+
+ test "Mastodon", %{conn: conn} do
+ user = insert(:user)
+ note_activity = insert(:note_activity, user: user)
+
+ resp =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/@#{user.nickname}/#{note_activity.id}")
+ |> response(200)
+
+ expected =
+ ""
+
+ assert resp =~ expected
+ end
+
+ test "Twitter", %{conn: conn} do
+ user = insert(:user)
+ note_activity = insert(:note_activity, user: user)
+
+ resp =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/#{user.nickname}/status/#{note_activity.id}")
+ |> response(200)
+
+ expected =
+ ""
+
+ assert resp =~ expected
+ end
+ end
end
diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs
index 82e955c25..4b3925ad2 100644
--- a/test/pleroma/web/plugs/frontend_static_plug_test.exs
+++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs
@@ -86,6 +86,8 @@ test "api routes are detected correctly" do
"objects",
"activities",
"notice",
+ "@:nickname",
+ ":nickname",
"users",
"tags",
"mailer",
@@ -94,8 +96,10 @@ test "api routes are detected correctly" do
"internal",
".well-known",
"nodeinfo",
+ "manifest.json",
"auth",
"proxy",
+ "phoenix",
"test",
"user_exists",
"check_password"
diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs
index d007e3f26..b7cfde1f7 100644
--- a/test/pleroma/web/plugs/rate_limiter_test.exs
+++ b/test/pleroma/web/plugs/rate_limiter_test.exs
@@ -48,6 +48,7 @@ test "it is enabled if remote_ip_found flag doesn't exist" do
refute RateLimiter.disabled?(build_conn())
end
+ @tag :erratic
test "it restricts based on config values" do
limiter_name = :test_plug_opts
scale = 80
@@ -137,6 +138,7 @@ test "it supports combination of options modifying bucket name" do
end
describe "unauthenticated users" do
+ @tag :erratic
test "are restricted based on remote IP" do
limiter_name = :test_unauthenticated
clear_config([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
@@ -174,6 +176,7 @@ test "are restricted based on remote IP" do
:ok
end
+ @tag :erratic
test "can have limits separate from unauthenticated connections" do
limiter_name = :test_authenticated1
@@ -199,6 +202,7 @@ test "can have limits separate from unauthenticated connections" do
assert conn.halted
end
+ @tag :erratic
test "different users are counted independently" do
limiter_name = :test_authenticated2
clear_config([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs
index 3380aec22..ee658ddf6 100644
--- a/test/pleroma/web/twitter_api/util_controller_test.exs
+++ b/test/pleroma/web/twitter_api/util_controller_test.exs
@@ -473,7 +473,10 @@ test "without permissions", %{conn: conn} do
test "with proper permissions and wrong or missing password", %{conn: conn} do
for params <- [%{"password" => "hi"}, %{}] do
- ret_conn = post(conn, "/api/pleroma/delete_account", params)
+ ret_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/delete_account", params)
assert json_response_and_validate_schema(ret_conn, 200) == %{
"error" => "Invalid password."
@@ -481,8 +484,28 @@ test "with proper permissions and wrong or missing password", %{conn: conn} do
end
end
- test "with proper permissions and valid password", %{conn: conn, user: user} do
- conn = post(conn, "/api/pleroma/delete_account?password=test")
+ test "with proper permissions and valid password (URL query)", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/delete_account?password=test")
+
+ ObanHelpers.perform_all()
+ assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
+
+ user = User.get_by_id(user.id)
+ refute user.is_active
+ assert user.name == nil
+ assert user.bio == ""
+ assert user.password_hash == nil
+ end
+
+ test "with proper permissions and valid password (JSON body)", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/delete_account", %{password: "test"})
+
ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 0c9783076..9fb41e985 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
-ExUnit.start(exclude: [:federated | os_exclude])
+ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude)
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)