Merge branch 'feature/user-status-subscriptions' into 'develop'
Add ability to subscribe to users See merge request pleroma/pleroma!1024
This commit is contained in:
commit
6504b43f96
|
@ -74,7 +74,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
|
||||||
* `confirm`
|
* `confirm`
|
||||||
* `captcha_solution`: optional, contains provider-specific captcha solution,
|
* `captcha_solution`: optional, contains provider-specific captcha solution,
|
||||||
* `captcha_token`: optional, contains provider-specific captcha token
|
* `captcha_token`: optional, contains provider-specific captcha token
|
||||||
* `token`: invite token required when the registerations aren't public.
|
* `token`: invite token required when the registrations aren't public.
|
||||||
* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
|
* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
|
||||||
* Example response:
|
* Example response:
|
||||||
```
|
```
|
||||||
|
@ -136,8 +136,57 @@ See [Admin-API](Admin-API.md)
|
||||||
* Method `POST`
|
* Method `POST`
|
||||||
* Authentication: required
|
* Authentication: required
|
||||||
* Params:
|
* Params:
|
||||||
* `id`: notifications's id
|
* `id`: notification's id
|
||||||
* Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}`
|
* Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}`
|
||||||
|
|
||||||
|
## `/api/v1/pleroma/accounts/:id/subscribe`
|
||||||
|
### Subscribe to receive notifications for all statuses posted by a user
|
||||||
|
* Method `POST`
|
||||||
|
* Authentication: required
|
||||||
|
* Params:
|
||||||
|
* `id`: account id to subscribe to
|
||||||
|
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
|
||||||
|
* Example response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "abcdefg",
|
||||||
|
"following": true,
|
||||||
|
"followed_by": false,
|
||||||
|
"blocking": false,
|
||||||
|
"muting": false,
|
||||||
|
"muting_notifications": false,
|
||||||
|
"subscribing": true,
|
||||||
|
"requested": false,
|
||||||
|
"domain_blocking": false,
|
||||||
|
"showing_reblogs": true,
|
||||||
|
"endorsed": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `/api/v1/pleroma/accounts/:id/unsubscribe`
|
||||||
|
### Unsubscribe to stop receiving notifications from user statuses
|
||||||
|
* Method `POST`
|
||||||
|
* Authentication: required
|
||||||
|
* Params:
|
||||||
|
* `id`: account id to unsubscribe from
|
||||||
|
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
|
||||||
|
* Example response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "abcdefg",
|
||||||
|
"following": true,
|
||||||
|
"followed_by": false,
|
||||||
|
"blocking": false,
|
||||||
|
"muting": false,
|
||||||
|
"muting_notifications": false,
|
||||||
|
"subscribing": false,
|
||||||
|
"requested": false,
|
||||||
|
"domain_blocking": false,
|
||||||
|
"showing_reblogs": true,
|
||||||
|
"endorsed": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## `/api/pleroma/notification_settings`
|
## `/api/pleroma/notification_settings`
|
||||||
### Updates user notification settings
|
### Updates user notification settings
|
||||||
* Method `PUT`
|
* Method `PUT`
|
||||||
|
|
|
@ -142,6 +142,7 @@ def get_notified_from_activity(
|
||||||
[]
|
[]
|
||||||
|> Utils.maybe_notify_to_recipients(activity)
|
|> Utils.maybe_notify_to_recipients(activity)
|
||||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||||
|
|> Utils.maybe_notify_subscribers(activity)
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|
|
||||||
User.get_users_from_set(recipients, local_only)
|
User.get_users_from_set(recipients, local_only)
|
||||||
|
|
|
@ -931,6 +931,38 @@ def unmute(muter, %{ap_id: ap_id}) do
|
||||||
update_and_set_cache(cng)
|
update_and_set_cache(cng)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribe(subscriber, %{ap_id: ap_id}) do
|
||||||
|
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||||
|
|
||||||
|
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
|
||||||
|
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
|
||||||
|
|
||||||
|
if blocked do
|
||||||
|
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
|
||||||
|
else
|
||||||
|
info_cng =
|
||||||
|
subscribed.info
|
||||||
|
|> User.Info.add_to_subscribers(subscriber.ap_id)
|
||||||
|
|
||||||
|
change(subscribed)
|
||||||
|
|> put_embed(:info, info_cng)
|
||||||
|
|> update_and_set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
|
||||||
|
with %User{} = user <- get_cached_by_ap_id(ap_id) do
|
||||||
|
info_cng =
|
||||||
|
user.info
|
||||||
|
|> User.Info.remove_from_subscribers(unsubscriber.ap_id)
|
||||||
|
|
||||||
|
change(user)
|
||||||
|
|> put_embed(:info, info_cng)
|
||||||
|
|> update_and_set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def block(blocker, %User{ap_id: ap_id} = blocked) do
|
def block(blocker, %User{ap_id: ap_id} = blocked) do
|
||||||
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
|
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
|
||||||
blocker =
|
blocker =
|
||||||
|
@ -941,6 +973,14 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
|
||||||
blocker
|
blocker
|
||||||
end
|
end
|
||||||
|
|
||||||
|
blocker =
|
||||||
|
if subscribed_to?(blocked, blocker) do
|
||||||
|
{:ok, blocker} = unsubscribe(blocked, blocker)
|
||||||
|
blocker
|
||||||
|
else
|
||||||
|
blocker
|
||||||
|
end
|
||||||
|
|
||||||
if following?(blocked, blocker) do
|
if following?(blocked, blocker) do
|
||||||
unfollow(blocked, blocker)
|
unfollow(blocked, blocker)
|
||||||
end
|
end
|
||||||
|
@ -989,12 +1029,21 @@ def blocks?(user, %{ap_id: ap_id}) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribed_to?(user, %{ap_id: ap_id}) do
|
||||||
|
with %User{} = target <- User.get_by_ap_id(ap_id) do
|
||||||
|
Enum.member?(target.info.subscribers, user.ap_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def muted_users(user),
|
def muted_users(user),
|
||||||
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
|
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
|
||||||
|
|
||||||
def blocked_users(user),
|
def blocked_users(user),
|
||||||
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
|
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
|
||||||
|
|
||||||
|
def subscribers(user),
|
||||||
|
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
|
||||||
|
|
||||||
def block_domain(user, domain) do
|
def block_domain(user, domain) do
|
||||||
info_cng =
|
info_cng =
|
||||||
user.info
|
user.info
|
||||||
|
|
|
@ -22,6 +22,7 @@ defmodule Pleroma.User.Info do
|
||||||
field(:domain_blocks, {:array, :string}, default: [])
|
field(:domain_blocks, {:array, :string}, default: [])
|
||||||
field(:mutes, {:array, :string}, default: [])
|
field(:mutes, {:array, :string}, default: [])
|
||||||
field(:muted_reblogs, {:array, :string}, default: [])
|
field(:muted_reblogs, {:array, :string}, default: [])
|
||||||
|
field(:subscribers, {:array, :string}, default: [])
|
||||||
field(:deactivated, :boolean, default: false)
|
field(:deactivated, :boolean, default: false)
|
||||||
field(:no_rich_text, :boolean, default: false)
|
field(:no_rich_text, :boolean, default: false)
|
||||||
field(:ap_enabled, :boolean, default: false)
|
field(:ap_enabled, :boolean, default: false)
|
||||||
|
@ -110,6 +111,14 @@ def set_blocks(info, blocks) do
|
||||||
|> validate_required([:blocks])
|
|> validate_required([:blocks])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_subscribers(info, subscribers) do
|
||||||
|
params = %{subscribers: subscribers}
|
||||||
|
|
||||||
|
info
|
||||||
|
|> cast(params, [:subscribers])
|
||||||
|
|> validate_required([:subscribers])
|
||||||
|
end
|
||||||
|
|
||||||
def add_to_mutes(info, muted) do
|
def add_to_mutes(info, muted) do
|
||||||
set_mutes(info, Enum.uniq([muted | info.mutes]))
|
set_mutes(info, Enum.uniq([muted | info.mutes]))
|
||||||
end
|
end
|
||||||
|
@ -126,6 +135,14 @@ def remove_from_block(info, blocked) do
|
||||||
set_blocks(info, List.delete(info.blocks, blocked))
|
set_blocks(info, List.delete(info.blocks, blocked))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_to_subscribers(info, subscribed) do
|
||||||
|
set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_subscribers(info, subscribed) do
|
||||||
|
set_subscribers(info, List.delete(info.subscribers, subscribed))
|
||||||
|
end
|
||||||
|
|
||||||
def set_domain_blocks(info, domain_blocks) do
|
def set_domain_blocks(info, domain_blocks) do
|
||||||
params = %{domain_blocks: domain_blocks}
|
params = %{domain_blocks: domain_blocks}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
|
@ -335,6 +336,24 @@ def maybe_notify_mentioned_recipients(
|
||||||
|
|
||||||
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
|
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
|
||||||
|
|
||||||
|
def maybe_notify_subscribers(
|
||||||
|
recipients,
|
||||||
|
%Activity{data: %{"actor" => actor, "type" => type}} = activity
|
||||||
|
)
|
||||||
|
when type == "Create" do
|
||||||
|
with %User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||||
|
subscriber_ids =
|
||||||
|
user
|
||||||
|
|> User.subscribers()
|
||||||
|
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|
||||||
|
|> Enum.map(& &1.ap_id)
|
||||||
|
|
||||||
|
recipients ++ subscriber_ids
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_notify_subscribers(recipients, _), do: recipients
|
||||||
|
|
||||||
def maybe_extract_mentions(%{"tag" => tag}) do
|
def maybe_extract_mentions(%{"tag" => tag}) do
|
||||||
tag
|
tag
|
||||||
|> Enum.filter(fn x -> is_map(x) end)
|
|> Enum.filter(fn x -> is_map(x) end)
|
||||||
|
|
|
@ -931,6 +931,34 @@ def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) d
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
with %User{} = subscription_target <- User.get_cached_by_id(id),
|
||||||
|
{:ok, subscription_target} = User.subscribe(user, subscription_target) do
|
||||||
|
conn
|
||||||
|
|> put_view(AccountView)
|
||||||
|
|> render("relationship.json", %{user: user, target: subscription_target})
|
||||||
|
else
|
||||||
|
{:error, message} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(403, Jason.encode!(%{"error" => message}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
with %User{} = subscription_target <- User.get_cached_by_id(id),
|
||||||
|
{:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
|
||||||
|
conn
|
||||||
|
|> put_view(AccountView)
|
||||||
|
|> render("relationship.json", %{user: user, target: subscription_target})
|
||||||
|
else
|
||||||
|
{:error, message} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(403, Jason.encode!(%{"error" => message}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def status_search(user, query) do
|
def status_search(user, query) do
|
||||||
fetched =
|
fetched =
|
||||||
if Regex.match?(~r/https?:/, query) do
|
if Regex.match?(~r/https?:/, query) do
|
||||||
|
|
|
@ -53,6 +53,7 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
|
||||||
blocking: User.blocks?(user, target),
|
blocking: User.blocks?(user, target),
|
||||||
muting: User.mutes?(user, target),
|
muting: User.mutes?(user, target),
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
|
subscribing: User.subscribed_to?(user, target),
|
||||||
requested: requested,
|
requested: requested,
|
||||||
domain_blocking: false,
|
domain_blocking: false,
|
||||||
showing_reblogs: User.showing_reblogs?(user, target),
|
showing_reblogs: User.showing_reblogs?(user, target),
|
||||||
|
|
|
@ -337,6 +337,9 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
post("/domain_blocks", MastodonAPIController, :block_domain)
|
post("/domain_blocks", MastodonAPIController, :block_domain)
|
||||||
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
|
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
|
||||||
|
|
||||||
|
post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
|
||||||
|
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddIndexOnSubscribers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
def change do
|
||||||
|
create index(:users, ["(info->'subscribers')"], name: :users_subscribers_index, using: :gin, concurrently: true)
|
||||||
|
end
|
||||||
|
end
|
|
@ -29,6 +29,18 @@ test "notifies someone when they are directly addressed" do
|
||||||
assert notification.activity_id == activity.id
|
assert notification.activity_id == activity.id
|
||||||
assert other_notification.activity_id == activity.id
|
assert other_notification.activity_id == activity.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it creates a notification for subscribed users" do
|
||||||
|
user = insert(:user)
|
||||||
|
subscriber = insert(:user)
|
||||||
|
|
||||||
|
User.subscribe(subscriber, user)
|
||||||
|
|
||||||
|
{:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
|
||||||
|
{:ok, [notification]} = Notification.create_notifications(status)
|
||||||
|
|
||||||
|
assert notification.user_id == subscriber.id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "create_notification" do
|
describe "create_notification" do
|
||||||
|
@ -153,6 +165,28 @@ test "it doesn't create a notification for repeat-unrepeat-repeat chains" do
|
||||||
{:ok, dupe} = TwitterAPI.repeat(user, status.id)
|
{:ok, dupe} = TwitterAPI.repeat(user, status.id)
|
||||||
assert nil == Notification.create_notification(dupe, retweeted_user)
|
assert nil == Notification.create_notification(dupe, retweeted_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it doesn't create duplicate notifications for follow+subscribed users" do
|
||||||
|
user = insert(:user)
|
||||||
|
subscriber = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _, _, _} = TwitterAPI.follow(subscriber, %{"user_id" => user.id})
|
||||||
|
User.subscribe(subscriber, user)
|
||||||
|
{:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
|
||||||
|
{:ok, [_notif]} = Notification.create_notifications(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it doesn't create subscription notifications if the recipient cannot see the status" do
|
||||||
|
user = insert(:user)
|
||||||
|
subscriber = insert(:user)
|
||||||
|
|
||||||
|
User.subscribe(subscriber, user)
|
||||||
|
|
||||||
|
{:ok, status} =
|
||||||
|
TwitterAPI.create_status(user, %{"status" => "inwisible", "visibility" => "direct"})
|
||||||
|
|
||||||
|
assert {:ok, []} == Notification.create_notifications(status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get notification" do
|
describe "get notification" do
|
||||||
|
|
|
@ -146,6 +146,15 @@ test "can't follow a user who blocked us" do
|
||||||
{:error, _} = User.follow(blockee, blocker)
|
{:error, _} = User.follow(blockee, blocker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can't subscribe to a user who blocked us" do
|
||||||
|
blocker = insert(:user)
|
||||||
|
blocked = insert(:user)
|
||||||
|
|
||||||
|
{:ok, blocker} = User.block(blocker, blocked)
|
||||||
|
|
||||||
|
{:error, _} = User.subscribe(blocked, blocker)
|
||||||
|
end
|
||||||
|
|
||||||
test "local users do not automatically follow local locked accounts" do
|
test "local users do not automatically follow local locked accounts" do
|
||||||
follower = insert(:user, info: %{locked: true})
|
follower = insert(:user, info: %{locked: true})
|
||||||
followed = insert(:user, info: %{locked: true})
|
followed = insert(:user, info: %{locked: true})
|
||||||
|
@ -729,6 +738,22 @@ test "blocks tear down blocked->blocker follow relationships" do
|
||||||
refute User.following?(blocker, blocked)
|
refute User.following?(blocker, blocked)
|
||||||
refute User.following?(blocked, blocker)
|
refute User.following?(blocked, blocker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "blocks tear down blocked->blocker subscription relationships" do
|
||||||
|
blocker = insert(:user)
|
||||||
|
blocked = insert(:user)
|
||||||
|
|
||||||
|
{:ok, blocker} = User.subscribe(blocked, blocker)
|
||||||
|
|
||||||
|
assert User.subscribed_to?(blocked, blocker)
|
||||||
|
refute User.subscribed_to?(blocker, blocked)
|
||||||
|
|
||||||
|
{:ok, blocker} = User.block(blocker, blocked)
|
||||||
|
|
||||||
|
assert User.blocks?(blocker, blocked)
|
||||||
|
refute User.subscribed_to?(blocker, blocked)
|
||||||
|
refute User.subscribed_to?(blocked, blocker)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "domain blocking" do
|
describe "domain blocking" do
|
||||||
|
|
|
@ -156,6 +156,7 @@ test "represent a relationship" do
|
||||||
blocking: true,
|
blocking: true,
|
||||||
muting: false,
|
muting: false,
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
|
subscribing: false,
|
||||||
requested: false,
|
requested: false,
|
||||||
domain_blocking: false,
|
domain_blocking: false,
|
||||||
showing_reblogs: true,
|
showing_reblogs: true,
|
||||||
|
@ -212,6 +213,7 @@ test "represent an embedded relationship" do
|
||||||
following: false,
|
following: false,
|
||||||
followed_by: false,
|
followed_by: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
|
subscribing: false,
|
||||||
muting: false,
|
muting: false,
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
requested: false,
|
requested: false,
|
||||||
|
|
|
@ -1556,6 +1556,25 @@ test "muting / unmuting a user", %{conn: conn} do
|
||||||
assert %{"id" => _id, "muting" => false} = json_response(conn, 200)
|
assert %{"id" => _id, "muting" => false} = json_response(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "subscribing / unsubscribing to a user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
subscription_target = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
test "getting a list of mutes", %{conn: conn} do
|
test "getting a list of mutes", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
Loading…
Reference in New Issue