Merge pull request 'Cherry-Picked Merges from Alex Gleason' (#5) from features/cherry-pick-merges into develop
Reviewed-on: https://codeberg.org/newroma-dev/newroma/pulls/5 Reviewed-by: floatingghost <floatingghost@noreply.codeberg.org>
This commit is contained in:
commit
9fdf0720cf
21 changed files with 679 additions and 23 deletions
|
@ -18,6 +18,7 @@ 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
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
347
docs/development/API/nodeinfo.md
Normal file
347
docs/development/API/nodeinfo.md
Normal 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -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": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
52
lib/pleroma/user_note.ex
Normal file
52
lib/pleroma/user_note.ex
Normal 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
|
|
@ -200,7 +200,7 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) 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)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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([])
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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" => %{}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" => []
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,10 @@ 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
|
||||||
]
|
]
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -456,6 +456,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)
|
||||||
|
|
15
priv/repo/migrations/20211121000000_create_user_notes.exs
Normal file
15
priv/repo/migrations/20211121000000_create_user_notes.exs
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -268,10 +268,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 +295,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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue