Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into finch

This commit is contained in:
Lain Soykaf 2021-12-26 16:41:17 +01:00
commit 6efbd08854
67 changed files with 1723 additions and 87 deletions

View File

@ -79,7 +79,6 @@ unit-testing:
- "**/*.ex" - "**/*.ex"
- "**/*.exs" - "**/*.exs"
- "mix.lock" - "mix.lock"
retry: 2
cache: &testing_cache_policy cache: &testing_cache_policy
<<: *global_cache_policy <<: *global_cache_policy
policy: pull policy: pull
@ -117,7 +116,6 @@ unit-testing-rum:
- "**/*.ex" - "**/*.ex"
- "**/*.exs" - "**/*.exs"
- "mix.lock" - "mix.lock"
retry: 2
cache: *testing_cache_policy cache: *testing_cache_policy
services: services:
- name: minibikini/postgres-with-rum:12 - name: minibikini/postgres-with-rum:12

View File

@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies - 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 ### 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. - Attachment dimensions and blurhashes are federated when available.
- Mastodon API: support `poll` notification. - Mastodon API: support `poll` notification.
- Pinned posts federation - Pinned posts federation
- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis
### Fixed ### Fixed
- Don't crash so hard when email settings are invalid. - Don't crash so hard when email settings are invalid.

View File

@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev &&\
mkdir release &&\ mkdir release &&\
mix release --path release mix release --path release
FROM alpine:3.11 FROM alpine:3.14
ARG BUILD_DATE ARG BUILD_DATE
ARG VCS_REF ARG VCS_REF
@ -31,8 +31,7 @@ LABEL maintainer="ops@pleroma.social" \
ARG HOME=/opt/pleroma ARG HOME=/opt/pleroma
ARG DATA=/var/lib/pleroma ARG DATA=/var/lib/pleroma
RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ RUN apk update &&\
apk update &&\
apk add exiftool ffmpeg imagemagick libmagic ncurses postgresql-client &&\ apk add exiftool ffmpeg imagemagick libmagic ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/uploads &&\

View File

@ -139,6 +139,7 @@
], ],
protocol: "https", protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl", secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
live_view: [signing_salt: "U5ELgdEwTD3n1+D5s0rY0AMg8/y1STxZ3Zvsl3bWh+oBcGrYdil0rXqPMRd3Glcq"],
signing_salt: "CqaoopA2", signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)], render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub_server: Pleroma.PubSub, pubsub_server: Pleroma.PubSub,
@ -253,7 +254,8 @@
] ]
], ],
show_reactions: true, show_reactions: true,
password_reset_token_validity: 60 * 60 * 24 password_reset_token_validity: 60 * 60 * 24,
profile_directory: true
config :pleroma, :welcome, config :pleroma, :welcome,
direct_message: [ direct_message: [

View File

@ -936,6 +936,11 @@
key: :show_reactions, key: :show_reactions,
type: :boolean, type: :boolean,
description: "Let favourites and emoji reactions be viewed through the API." description: "Let favourites and emoji reactions be viewed through the API."
},
%{
key: :profile_directory,
type: :boolean,
description: "Enable profile directory."
} }
] ]
}, },

View File

@ -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` ## `GET /api/v1/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user ### Retrive the details of a user

View File

@ -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, `[]` - `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 ### Featured tags
*Added in Mastodon 3.0.0* *Added in Mastodon 3.0.0*

View File

@ -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"
}
```

View File

@ -159,10 +159,12 @@ See [Admin-API](admin_api.md)
"muting": false, "muting": false,
"muting_notifications": false, "muting_notifications": false,
"subscribing": true, "subscribing": true,
"notifying": true,
"requested": false, "requested": false,
"domain_blocking": false, "domain_blocking": false,
"showing_reblogs": true, "showing_reblogs": true,
"endorsed": false "endorsed": false,
"note": ""
} }
``` ```
@ -183,10 +185,12 @@ See [Admin-API](admin_api.md)
"muting": false, "muting": false,
"muting_notifications": false, "muting_notifications": false,
"subscribing": false, "subscribing": false,
"notifying": false,
"requested": false, "requested": false,
"domain_blocking": false, "domain_blocking": false,
"showing_reblogs": true, "showing_reblogs": true,
"endorsed": false "endorsed": false,
"note": ""
} }
``` ```

View File

@ -199,6 +199,7 @@ def run(["gen" | rest]) do
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) 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) 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) 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) {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates" template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
@ -217,6 +218,7 @@ def run(["gen" | rest]) do
secret: secret, secret: secret,
jwt_secret: jwt_secret, jwt_secret: jwt_secret,
signing_salt: signing_salt, 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_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false), web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
db_configurable?: db_configurable?, db_configurable?: db_configurable?,

View File

@ -9,7 +9,8 @@
mute: 2, mute: 2,
reblog_mute: 3, reblog_mute: 3,
notification_mute: 4, notification_mute: 4,
inverse_subscription: 5 inverse_subscription: 5,
suggestion_dismiss: 6
) )
defenum(Pleroma.FollowingRelationship.State, defenum(Pleroma.FollowingRelationship.State,

View File

@ -103,6 +103,7 @@ defp load_pack(pack_dir, emoji_groups) do
pack_file = Path.join(pack_dir, "pack.json") pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do if File.exists?(pack_file) do
Logger.info("Loading emoji pack from JSON: #{pack_file}")
contents = Jason.decode!(File.read!(pack_file)) contents = Jason.decode!(File.read!(pack_file))
contents["files"] contents["files"]
@ -115,6 +116,7 @@ defp load_pack(pack_dir, emoji_groups) do
emoji_txt = Path.join(pack_dir, "emoji.txt") emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do if File.exists?(emoji_txt) do
Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}")
load_from_file(emoji_txt, emoji_groups) load_from_file(emoji_txt, emoji_groups)
else else
extensions = Config.get([:emoji, :pack_extensions]) extensions = Config.get([:emoji, :pack_extensions])

View File

@ -338,6 +338,26 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}" "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end 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{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},

View File

@ -148,6 +148,8 @@ defmodule Pleroma.User do
field(:last_active_at, :naive_datetime) field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true) field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{}) field(:pinned_objects, :map, default: %{})
field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime)
embeds_one( embeds_one(
:notification_settings, :notification_settings,
@ -1676,6 +1678,22 @@ def confirm(%User{is_confirmed: false} = user) do
def confirm(%User{} = user), do: {:ok, user} 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 def update_notification_settings(%User{} = user, settings) do
user user
|> cast(%{notification_settings: settings}, []) |> cast(%{notification_settings: settings}, [])
@ -2482,4 +2500,16 @@ def active_user_count(days \\ 30) do
|> where([u], u.local == true) |> where([u], u.local == true)
|> Repo.aggregate(:count) |> Repo.aggregate(:count)
end 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 end

View File

@ -46,6 +46,8 @@ defmodule Pleroma.User.Query do
unconfirmed: boolean(), unconfirmed: boolean(),
is_admin: boolean(), is_admin: boolean(),
is_moderator: boolean(), is_moderator: boolean(),
is_suggested: boolean(),
is_discoverable: boolean(),
super_users: boolean(), super_users: boolean(),
invisible: boolean(), invisible: boolean(),
internal: boolean(), internal: boolean(),
@ -167,6 +169,14 @@ defp compose_query({:unconfirmed, _}, query) do
where(query, [u], u.is_confirmed == false) where(query, [u], u.is_confirmed == false)
end 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 defp compose_query({:followers, %User{id: id}}, query) do
query query
|> where([u], u.id != ^id) |> where([u], u.id != ^id)

52
lib/pleroma/user_note.ex Normal file
View File

@ -0,0 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -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} if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end 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(%{ defp increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object, "object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create" "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), _ <- increase_replies_count_if_reply(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity}, {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity), {:ok, _actor} <- increase_note_count_if_public(actor, activity),
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity), :ok <- maybe_schedule_poll_notifications(activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do

View File

@ -199,8 +199,9 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, notifications} = Notification.create_notifications(activity, do_send: false) {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object) {: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) Object.increase_replies_count(in_reply_to)
end end

View File

@ -446,7 +446,7 @@ def update_follow_state_for_all(
|> Activity.Queries.by_type() |> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor) |> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object) |> 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)]) |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([]) |> Repo.update_all([])

View File

@ -35,7 +35,9 @@ defmodule Pleroma.Web.AdminAPI.UserController do
:toggle_activation, :toggle_activation,
:activate, :activate,
:deactivate, :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) render(conn, "index.json", users: updated_users)
end 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 def index(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters]) filters = maybe_parse_filters(params[:filters])

View File

@ -80,6 +80,7 @@ def render("show.json", %{user: user}) do
"tags" => user.tags || [], "tags" => user.tags || [],
"is_confirmed" => user.is_confirmed, "is_confirmed" => user.is_confirmed,
"is_approved" => user.is_approved, "is_approved" => user.is_approved,
"is_suggested" => user.is_suggested,
"url" => user.uri || user.ap_id, "url" => user.uri || user.ap_id,
"registration_reason" => user.registration_reason, "registration_reason" => user.registration_reason,
"actor_type" => user.actor_type, "actor_type" => user.actor_type,

View File

@ -226,6 +226,12 @@ def follow_operation do
type: :boolean, type: :boolean,
description: "Receive this account's reblogs in home timeline? Defaults to true.", description: "Receive this account's reblogs in home timeline? Defaults to true.",
default: 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 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 def follow_by_uri_operation do
%Operation{ %Operation{
tags: ["Account actions"], tags: ["Account actions"],
@ -685,9 +714,11 @@ defp array_of_relationships do
"blocked_by" => true, "blocked_by" => true,
"muting" => false, "muting" => false,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => false, "requested" => false,
"domain_blocking" => false, "domain_blocking" => false,
"subscribing" => false, "subscribing" => false,
"notifying" => false,
"endorsed" => true "endorsed" => true
}, },
%{ %{
@ -699,9 +730,11 @@ defp array_of_relationships do
"blocked_by" => true, "blocked_by" => true,
"muting" => true, "muting" => true,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => true, "requested" => true,
"domain_blocking" => false, "domain_blocking" => false,
"subscribing" => false, "subscribing" => false,
"notifying" => false,
"endorsed" => false "endorsed" => false
}, },
%{ %{
@ -713,9 +746,11 @@ defp array_of_relationships do
"blocked_by" => false, "blocked_by" => false,
"muting" => true, "muting" => true,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => false, "requested" => false,
"domain_blocking" => true, "domain_blocking" => true,
"subscribing" => true, "subscribing" => true,
"notifying" => true,
"endorsed" => false "endorsed" => false
} }
] ]
@ -760,6 +795,23 @@ defp mute_request do
} }
end 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 defp array_of_lists do
%Schema{ %Schema{
title: "ArrayOfLists", title: "ArrayOfLists",

View File

@ -216,7 +216,71 @@ def approve_operation do
request_body( request_body(
"Parameters", "Parameters",
%Schema{ %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, type: :object,
properties: %{ properties: %{
nicknames: %Schema{ nicknames: %Schema{

View File

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -191,6 +191,7 @@ def delete_account_operation do
parameters: [ parameters: [
Operation.parameter(:password, :query, :string, "Password") Operation.parameter(:password, :query, :string, "Password")
], ],
requestBody: request_body("Parameters", delete_account_request(), required: false),
responses: %{ responses: %{
200 => 200 =>
Operation.response("Success", "application/json", %Schema{ 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})} responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
} }
end 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 end

View File

@ -194,9 +194,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"id" => "9tKi3esbG7OQgZ2920", "id" => "9tKi3esbG7OQgZ2920",
"muting" => false, "muting" => false,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => false, "requested" => false,
"showing_reblogs" => true, "showing_reblogs" => true,
"subscribing" => false "subscribing" => false,
"notifying" => false
}, },
"settings_store" => %{ "settings_store" => %{
"pleroma-fe" => %{} "pleroma-fe" => %{}

View File

@ -22,9 +22,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
id: FlakeID, id: FlakeID,
muting: %Schema{type: :boolean}, muting: %Schema{type: :boolean},
muting_notifications: %Schema{type: :boolean}, muting_notifications: %Schema{type: :boolean},
note: %Schema{type: :string},
requested: %Schema{type: :boolean}, requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean}, showing_reblogs: %Schema{type: :boolean},
subscribing: %Schema{type: :boolean} subscribing: %Schema{type: :boolean},
notifying: %Schema{type: :boolean}
}, },
example: %{ example: %{
"blocked_by" => false, "blocked_by" => false,
@ -36,9 +38,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
"id" => "9tKi3esbG7OQgZ2920", "id" => "9tKi3esbG7OQgZ2920",
"muting" => false, "muting" => false,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => false, "requested" => false,
"showing_reblogs" => true, "showing_reblogs" => true,
"subscribing" => false "subscribing" => false,
"notifying" => false
} }
}) })
end end

View File

@ -282,9 +282,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"id" => "9toJCsKN7SmSf3aj5c", "id" => "9toJCsKN7SmSf3aj5c",
"muting" => false, "muting" => false,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "",
"requested" => false, "requested" => false,
"showing_reblogs" => true, "showing_reblogs" => true,
"subscribing" => false "subscribing" => false,
"notifying" => false
}, },
"skip_thread_containment" => false, "skip_thread_containment" => false,
"tags" => [] "tags" => []

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config alias Pleroma.Config
socket("/socket", Pleroma.Web.UserSocket) socket("/socket", Pleroma.Web.UserSocket)
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])

View File

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserNote
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
@ -53,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
when action in [:verify_credentials, :endorsements, :identity_proofs] 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) 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]) plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow] @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( plug(
RateLimiter, RateLimiter,
@ -435,6 +440,16 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
end end
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" @doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do case User.get_cached_by_nickname(uri) do

View File

@ -0,0 +1,82 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -17,6 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger require Logger
@search_limit 40
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) # 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], resolve: params[:resolve],
following: params[:following], following: params[:following],
limit: params[:limit], limit: min(params[:limit], @search_limit),
offset: params[:offset], offset: params[:offset],
type: params[:type], type: params[:type],
author: get_author(params), author: get_author(params),

View File

@ -4,11 +4,16 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Ecto.Query
alias Pleroma.FollowingRelationship
alias Pleroma.User
alias Pleroma.UserRelationship
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate) 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 def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation") operation = String.to_existing_atom("#{action}_operation")
@ -26,7 +31,90 @@ def index_operation do
} }
end 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" @doc "GET /api/v1/suggestions"
def index(conn, params), def index(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(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 end

View File

@ -24,6 +24,7 @@ def follow(follower, followed, params \\ %{}) do
with {:ok, follower, _followed, _} <- result do with {:ok, follower, _followed, _} <- result do
options = cast_params(params) options = cast_params(params)
set_reblogs_visibility(options[:reblogs], result) set_reblogs_visibility(options[:reblogs], result)
set_subscription(options[:notify], result)
{:ok, follower} {:ok, follower}
end end
end end
@ -36,6 +37,16 @@ defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
CommonAPI.show_reblogs(follower, followed) CommonAPI.show_reblogs(follower, followed)
end 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()) @spec get_followers(User.t(), map()) :: list(User.t())
def get_followers(user, params \\ %{}) do def get_followers(user, params \\ %{}) do
user user
@ -73,7 +84,8 @@ defp cast_params(params) do
exclude_visibilities: {:array, :string}, exclude_visibilities: {:array, :string},
reblogs: :boolean, reblogs: :boolean,
with_muted: :boolean, with_muted: :boolean,
account_ap_id: :string account_ap_id: :string,
notify: :boolean
} }
changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset = cast({%{}, param_types}, params, Map.keys(param_types))

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserNote
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
@ -101,6 +102,15 @@ def render(
User.following?(target, reading_user) User.following?(target, reading_user)
end 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 # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{ %{
id: to_string(target.id), id: to_string(target.id),
@ -138,14 +148,8 @@ def render(
target, target,
&User.muted_notifications?(&1, &2) &User.muted_notifications?(&1, &2)
), ),
subscribing: subscribing: subscribing,
UserRelationship.exists?( notifying: subscribing,
user_relationships,
:inverse_subscription,
target,
reading_user,
&User.subscribed_to?(&2, &1)
),
requested: follow_state == :follow_pending, requested: follow_state == :follow_pending,
domain_blocking: User.blocks_domain?(reading_user, target), domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs: showing_reblogs:
@ -156,7 +160,12 @@ def render(
target, target,
&User.muting_reblogs?(&1, &2) &User.muting_reblogs?(&1, &2)
), ),
endorsed: false endorsed: false,
note:
UserNote.show(
reading_user,
target
)
} }
end end
@ -261,6 +270,7 @@ defp do_render("show.json", %{user: user} = opts) do
actor_type: user.actor_type actor_type: user.actor_type
} }
}, },
last_status_at: user.last_status_at,
# Pleroma extensions # Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub # 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, ap_id: user.ap_id,
also_known_as: user.also_known_as, also_known_as: user.also_known_as,
is_confirmed: user.is_confirmed, is_confirmed: user.is_confirmed,
is_suggested: user.is_suggested,
tags: user.tags, tags: user.tags,
hide_followers_count: user.hide_followers_count, hide_followers_count: user.hide_followers_count,
hide_follows_count: user.hide_follows_count, hide_follows_count: user.hide_follows_count,

View File

@ -59,6 +59,7 @@ def features do
"mastodon_api", "mastodon_api",
"mastodon_api_streaming", "mastodon_api_streaming",
"polls", "polls",
"v2_suggestions",
"pleroma_explicit_addressing", "pleroma_explicit_addressing",
"shareable_emoji_packs", "shareable_emoji_packs",
"multifetch", "multifetch",
@ -83,7 +84,13 @@ def features do
"safe_dm_mentions" "safe_dm_mentions"
end, end,
"pleroma_emoji_reactions", "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) |> Enum.filter(& &1)
end end

View File

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -151,7 +151,9 @@ def index2(%{assigns: %{user: user}} = conn, params) do
index_query(user, params) index_query(user, params)
|> Pagination.fetch_paginated(params) |> Pagination.fetch_paginated(params)
render(conn, "index.json", chats: chats) conn
|> add_link_headers(chats)
|> render("index.json", chats: chats)
end end
defp index_query(%{id: user_id} = user, params) do defp index_query(%{id: user_id} = user, params) do

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Router do defmodule Pleroma.Web.Router do
use Pleroma.Web, :router use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :accepts_html do pipeline :accepts_html do
plug(:accepts, ["html"]) plug(:accepts, ["html"])
@ -150,6 +151,7 @@ defmodule Pleroma.Web.Router do
get("/emoji", UtilController, :emoji) get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha) get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck) get("/healthcheck", UtilController, :healthcheck)
post("/remote_interaction", UtilController, :remote_interaction)
end end
scope "/api/v1/pleroma", Pleroma.Web do scope "/api/v1/pleroma", Pleroma.Web do
@ -157,12 +159,11 @@ defmodule Pleroma.Web.Router do
post("/uploader_callback/:upload_path", UploaderController, :callback) post("/uploader_callback/:upload_path", UploaderController, :callback)
end end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin]) pipe_through([:admin_api, :require_admin])
put("/users/disable_mfa", AdminAPIController, :disable_mfa) 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", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/: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/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow) post("/users/unfollow", UserController, :unfollow)
delete("/users", UserController, :delete)
post("/users", UserController, :create) 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/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate) patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate) patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve) patch("/users/approve", UserController, :approve)
get("/relay", RelayController, :index) delete("/users", UserController, :delete)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
post("/users/invite_token", InviteController, :create) post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index) get("/users/invites", InviteController, :index)
@ -214,13 +260,6 @@ defmodule Pleroma.Web.Router do
get("/instances/:instance/statuses", InstanceController, :list_statuses) get("/instances/:instance/statuses", InstanceController, :list_statuses)
delete("/instances/:instance", InstanceController, :delete) 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", ReportController, :index)
get("/reports/:id", ReportController, :show) get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update) patch("/reports", ReportController, :update)
@ -232,39 +271,19 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id", StatusController, :delete) delete("/statuses/:id", StatusController, :delete)
get("/statuses", StatusController, :index) 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) get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji) post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats) 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", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages) get("/chats/:id/messages", ChatController, :messages)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message) delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
end end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do scope "/pack" do
pipe_through([:admin_api, :require_admin]) pipe_through(:admin_api)
post("/", EmojiPackController, :create) post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update) patch("/", EmojiPackController, :update)
@ -279,7 +298,7 @@ defmodule Pleroma.Web.Router do
# Modifying packs # Modifying packs
scope "/packs" do scope "/packs" do
pipe_through([:admin_api, :require_admin]) pipe_through(:admin_api)
get("/import", EmojiPackController, :import_from_filesystem) get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote) get("/remote", EmojiPackController, :remote)
@ -456,6 +475,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unblock", AccountController, :unblock) post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute) post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute) post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
get("/conversations", ConversationController, :index) get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read) post("/conversations/:id/read", ConversationController, :mark_as_read)
@ -535,6 +555,7 @@ defmodule Pleroma.Web.Router do
delete("/push/subscription", SubscriptionController, :delete) delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index) get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home) get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct) get("/timelines/direct", TimelineController, :direct)
@ -579,6 +600,8 @@ defmodule Pleroma.Web.Router do
get("/timelines/tag/:tag", TimelineController, :hashtag) get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show) get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index)
end end
scope "/api/v2", Pleroma.Web.MastodonAPI do scope "/api/v2", Pleroma.Web.MastodonAPI do
@ -586,6 +609,8 @@ defmodule Pleroma.Web.Router do
get("/search", SearchController, :search2) get("/search", SearchController, :search2)
post("/media", MediaController, :create2) post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
@ -627,6 +652,11 @@ defmodule Pleroma.Web.Router do
get("/activities/:uuid", OStatus.OStatusController, :activity) get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice) 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 # Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object) get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity) get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
@ -736,6 +766,12 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
end
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
pipe_through(:pleroma_html) pipe_through(:pleroma_html)
@ -757,6 +793,11 @@ defmodule Pleroma.Web.Router do
end end
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 # Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do if Pleroma.Config.get(:env) == :test do
@test_actions [ @test_actions [

View File

@ -167,6 +167,15 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id) 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), defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id) do: assign(conn, :username_or_id, user_id)

View File

@ -62,6 +62,15 @@ def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profil
end end
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 def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json") render(conn, "frontend_configurations.json")
end end
@ -123,8 +132,10 @@ def change_email(%{assigns: %{user: user}, body_params: body_params} = conn, %{}
end end
end end
def delete_account(%{assigns: %{user: user}} = conn, params) do def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
password = params[:password] || "" # 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 case CommonAPI.Utils.confirm_current_password(user, password) do
{:ok, user} -> {:ok, user} ->

View File

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

10
mix.exs
View File

@ -8,7 +8,7 @@ def project do
elixir: "~> 1.9", elixir: "~> 1.9",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(), 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]], xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
aliases: aliases(), aliases: aliases(),
@ -79,6 +79,7 @@ def application do
:comeonin, :comeonin,
:quack, :quack,
:fast_sanitize, :fast_sanitize,
:os_mon,
:ssl :ssl
], ],
included_applications: [:ex_syslogger] 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(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"] defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors(:prod), do: false defp warnings_as_errors, do: System.get_env("CI") == "true"
defp warnings_as_errors(_), do: true
# Specifies OAuth dependencies. # Specifies OAuth dependencies.
defp oauth_deps do defp oauth_deps do
@ -129,7 +129,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"}, {:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"}, {:fast_sanitize, "~> 0.2.0"},
{:html_entities, "~> 0.5", override: true}, {:html_entities, "~> 0.5", override: true},
{:phoenix_html, "~> 2.14"}, {:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"}, {:calendar, "~> 1.0"},
{:cachex, "~> 3.2"}, {:cachex, "~> 3.2"},
{:poison, "~> 3.0", override: true}, {:poison, "~> 3.0", override: true},
@ -196,6 +196,8 @@ defp deps do
{:majic, "~> 1.0"}, {:majic, "~> 1.0"},
{:eblurhash, "~> 1.1.0"}, {:eblurhash, "~> 1.1.0"},
{:open_api_spex, "~> 3.10"}, {:open_api_spex, "~> 3.10"},
{:phoenix_live_dashboard, "~> 0.6.2"},
{:ecto_psql_extras, "~> 0.6"},
# indirect dependency version override # indirect dependency version override
{:plug, "~> 1.10.4", override: true}, {:plug, "~> 1.10.4", override: true},

View File

@ -32,6 +32,7 @@
"eblurhash": {:hex, :eblurhash, "1.1.0", "e10ccae762598507ebfacf0b645ed49520f2afa3e7e9943e73a91117dffce415", [:rebar3], [], "hexpm", "2e6b889d09fddd374e3c5ac57c486138768763264e99ac1074ae5fa7fc9ab51d"}, "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": {: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_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"}, "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"}, "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"}, "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"}, "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": {: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_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_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"}, "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"}, "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"}, "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"}, "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"}, "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": {: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"}, "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"}, "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"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddIsDiscoverableIndexToUsers do
use Ecto.Migration
def change do
create(index(:users, [:is_discoverable]))
end
end

View File

@ -13,6 +13,7 @@ config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>], url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
http: [ip: {<%= String.replace(listen_ip, ".", ", ") %>}, port: <%= listen_port %>], http: [ip: {<%= String.replace(listen_ip, ".", ", ") %>}, port: <%= listen_port %>],
secret_key_base: "<%= secret %>", secret_key_base: "<%= secret %>",
live_view: [signing_salt: "<%= lv_signing_salt %>"],
signing_salt: "<%= signing_salt %>" signing_salt: "<%= signing_salt %>"
config :pleroma, :instance, config :pleroma, :instance,

View File

@ -82,6 +82,7 @@ test "transfer config values with full subkey update" do
on_exit(fn -> Restarter.Pleroma.refresh() end) on_exit(fn -> Restarter.Pleroma.refresh() end)
end end
@tag :erratic
test "don't restart if no reboot time settings were changed" do test "don't restart if no reboot time settings were changed" do
clear_config(:emoji) clear_config(:emoji)
insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) 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 end
@tag :erratic
test "on reboot time key" do test "on reboot time key" do
clear_config(:shout) clear_config(:shout)
insert(:config, key: :shout, value: [enabled: false]) insert(:config, key: :shout, value: [enabled: false])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end end
@tag :erratic
test "on reboot time subkey" do test "on reboot time subkey" do
clear_config(Pleroma.Captcha) clear_config(Pleroma.Captcha)
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end end
@tag :erratic
test "don't restart pleroma on reboot time key and subkey if there is false flag" do test "don't restart pleroma on reboot time key and subkey if there is false flag" do
clear_config(:shout) clear_config(:shout)
clear_config(Pleroma.Captcha) clear_config(Pleroma.Captcha)

View File

@ -34,4 +34,14 @@ test "it returns internal users when enabled" do
assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2
end end
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 end

View File

@ -1718,6 +1718,38 @@ test "delete/1 purges a remote user" do
assert user.banner == %{} assert user.banner == %{}
end 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 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") assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end end

View File

@ -88,6 +88,16 @@ test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do
assert User.blocks?(user, blocked) assert User.blocks?(user, blocked)
end 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", %{ test "it blocks but does not unfollow if the relevant setting is set", %{
user: user, user: user,
blocked: blocked, blocked: blocked,
@ -542,4 +552,74 @@ test "it streams out the announce", %{announce: announce} do
end end
end 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 end

View File

@ -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).data["state"] == "accept"
assert refresh_record(follow_activity_two).data["state"] == "accept" assert refresh_record(follow_activity_two).data["state"] == "accept"
end 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 end
describe "update_follow_state/2" do describe "update_follow_state/2" do

View File

@ -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}" "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
end 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 test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user) user = insert(:user)
@ -906,6 +956,7 @@ defp user_response(user, attrs \\ %{}) do
"display_name" => HTML.strip_tags(user.name || user.nickname), "display_name" => HTML.strip_tags(user.name || user.nickname),
"is_confirmed" => true, "is_confirmed" => true,
"is_approved" => true, "is_approved" => true,
"is_suggested" => false,
"url" => user.ap_id, "url" => user.ap_id,
"registration_reason" => nil, "registration_reason" => nil,
"actor_type" => "Person", "actor_type" => "Person",

View File

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -922,6 +922,27 @@ test "following with reblogs" do
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end 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 test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow # self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/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 assert [%{"id" => ^id2}] = result
end 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 end

View File

@ -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

View File

@ -4,8 +4,11 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
use Pleroma.Web.ConnCase, async: true 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 test "returns empty result", %{conn: conn} do
res = res =
@ -15,4 +18,66 @@ test "returns empty result", %{conn: conn} do
assert res == [] assert res == []
end 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 end

View File

@ -74,6 +74,7 @@ test "Represent a user account" do
fields: [] fields: []
}, },
fqn: "shp@shitposter.club", fqn: "shp@shitposter.club",
last_status_at: nil,
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,
also_known_as: ["https://shitposter.zone/users/shp"], also_known_as: ["https://shitposter.zone/users/shp"],
@ -83,6 +84,7 @@ test "Represent a user account" do
tags: [], tags: [],
is_admin: false, is_admin: false,
is_moderator: false, is_moderator: false,
is_suggested: false,
hide_favorites: true, hide_favorites: true,
hide_followers: false, hide_followers: false,
hide_follows: false, hide_follows: false,
@ -174,6 +176,7 @@ test "Represent a Service(bot) account" do
fields: [] fields: []
}, },
fqn: "shp@shitposter.club", fqn: "shp@shitposter.club",
last_status_at: nil,
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,
also_known_as: [], also_known_as: [],
@ -183,6 +186,7 @@ test "Represent a Service(bot) account" do
tags: [], tags: [],
is_admin: false, is_admin: false,
is_moderator: false, is_moderator: false,
is_suggested: false,
hide_favorites: true, hide_favorites: true,
hide_followers: false, hide_followers: false,
hide_follows: false, hide_follows: false,
@ -268,10 +272,12 @@ defp test_relationship_rendering(user, other_user, expected_result) do
muting: false, muting: false,
muting_notifications: false, muting_notifications: false,
subscribing: false, subscribing: false,
notifying: false,
requested: false, requested: false,
domain_blocking: false, domain_blocking: false,
showing_reblogs: true, showing_reblogs: true,
endorsed: false endorsed: false,
note: ""
} }
test "represent a relationship for the following and followed user" do 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: true,
muting_notifications: true, muting_notifications: true,
subscribing: true, subscribing: true,
notifying: true,
showing_reblogs: false, showing_reblogs: false,
id: to_string(other_user.id) id: to_string(other_user.id)
} }

View File

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -343,4 +343,54 @@ test "does not require authentication on non-federating instances", %{
|> response(200) |> response(200)
end end
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 =
"<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
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 =
"<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
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 =
"<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
assert resp =~ expected
end
end
end end

View File

@ -86,6 +86,8 @@ test "api routes are detected correctly" do
"objects", "objects",
"activities", "activities",
"notice", "notice",
"@:nickname",
":nickname",
"users", "users",
"tags", "tags",
"mailer", "mailer",
@ -94,8 +96,10 @@ test "api routes are detected correctly" do
"internal", "internal",
".well-known", ".well-known",
"nodeinfo", "nodeinfo",
"manifest.json",
"auth", "auth",
"proxy", "proxy",
"phoenix",
"test", "test",
"user_exists", "user_exists",
"check_password" "check_password"

View File

@ -48,6 +48,7 @@ test "it is enabled if remote_ip_found flag doesn't exist" do
refute RateLimiter.disabled?(build_conn()) refute RateLimiter.disabled?(build_conn())
end end
@tag :erratic
test "it restricts based on config values" do test "it restricts based on config values" do
limiter_name = :test_plug_opts limiter_name = :test_plug_opts
scale = 80 scale = 80
@ -137,6 +138,7 @@ test "it supports combination of options modifying bucket name" do
end end
describe "unauthenticated users" do describe "unauthenticated users" do
@tag :erratic
test "are restricted based on remote IP" do test "are restricted based on remote IP" do
limiter_name = :test_unauthenticated limiter_name = :test_unauthenticated
clear_config([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) clear_config([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
@ -174,6 +176,7 @@ test "are restricted based on remote IP" do
:ok :ok
end end
@tag :erratic
test "can have limits separate from unauthenticated connections" do test "can have limits separate from unauthenticated connections" do
limiter_name = :test_authenticated1 limiter_name = :test_authenticated1
@ -199,6 +202,7 @@ test "can have limits separate from unauthenticated connections" do
assert conn.halted assert conn.halted
end end
@tag :erratic
test "different users are counted independently" do test "different users are counted independently" do
limiter_name = :test_authenticated2 limiter_name = :test_authenticated2
clear_config([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) clear_config([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])

View File

@ -473,7 +473,10 @@ test "without permissions", %{conn: conn} do
test "with proper permissions and wrong or missing password", %{conn: conn} do test "with proper permissions and wrong or missing password", %{conn: conn} do
for params <- [%{"password" => "hi"}, %{}] 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) == %{ assert json_response_and_validate_schema(ret_conn, 200) == %{
"error" => "Invalid password." "error" => "Invalid password."
@ -481,8 +484,28 @@ test "with proper permissions and wrong or missing password", %{conn: conn} do
end end
end end
test "with proper permissions and valid password", %{conn: conn, user: user} do test "with proper permissions and valid password (URL query)", %{conn: conn, user: user} do
conn = post(conn, "/api/pleroma/delete_account?password=test") 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() ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] 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) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)