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

This commit is contained in:
a1batross 2020-11-05 18:22:10 +01:00
commit 30a1d4aa98
40 changed files with 593 additions and 87 deletions

View File

@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- App metrics: ability to restrict access to specified IP whitelist. - App metrics: ability to restrict access to specified IP whitelist.
- Account backup - Account backup
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance. - Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
- Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`
### Changed ### Changed
@ -30,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Users with the `discoverable` field set to false will not show up in searches. - Users with the `discoverable` field set to false will not show up in searches.
- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option).
- Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`. - Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`.
- Polls now always return a `voters_count`, even if they are single-choice
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -40,6 +42,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: (`GET /api/pleroma/admin/users`) added filters user by `unconfirmed` status - Admin API: (`GET /api/pleroma/admin/users`) added filters user by `unconfirmed` status
- Admin API: (`GET /api/pleroma/admin/users`) added filters user by `actor_type` - Admin API: (`GET /api/pleroma/admin/users`) added filters user by `actor_type`
- Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending. - Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending.
- Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances.
- Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
</details> </details>

View File

@ -562,7 +562,8 @@ config :pleroma, Oban,
background: 5, background: 5,
remote_fetcher: 2, remote_fetcher: 2,
attachments_cleanup: 5, attachments_cleanup: 5,
new_users_digest: 1 new_users_digest: 1,
mute_expire: 5
], ],
plugins: [Oban.Plugins.Pruner], plugins: [Oban.Plugins.Pruner],
crontab: [ crontab: [
@ -820,7 +821,7 @@ config :pleroma, :restrict_unauthenticated,
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
config :pleroma, :mrf, config :pleroma, :mrf,
policies: Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy],
transparency: true, transparency: true,
transparency_exclusions: [] transparency_exclusions: []

View File

@ -116,6 +116,10 @@ The modified chat message
This will return a list of chats that you have been involved in, sorted by their This will return a list of chats that you have been involved in, sorted by their
last update (so new chats will be at the top). last update (so new chats will be at the top).
Parameters:
- with_muted: Include chats from muted users (boolean).
Returned data: Returned data:
```json ```json

View File

@ -9,9 +9,13 @@ Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mas
## Timelines ## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`. Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you. Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
Adding the parameter `instance=lain.com` to the public timeline will show only statuses originating from `lain.com` (or any remote instance).
## Statuses ## Statuses
- `visibility`: has an additional possible value `list` - `visibility`: has an additional possible value `list`
@ -249,6 +253,12 @@ Has these additional fields under the `pleroma` object:
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`.
## User muting and thread muting
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
## Not implemented ## Not implemented
Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority. Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority.

View File

@ -0,0 +1,136 @@
# Configuring Ejabberd (XMPP Server) to use Pleroma for authentication
If you want to give your Pleroma users an XMPP (chat) account, you can configure [Ejabberd](https://github.com/processone/ejabberd) to use your Pleroma server for user authentication, automatically giving every local user an XMPP account.
In general, you just have to follow the configuration described at [https://docs.ejabberd.im/admin/configuration/authentication/#external-script](https://docs.ejabberd.im/admin/configuration/authentication/#external-script). Please read this section carefully.
Copy the script below to suitable path on your system and set owner and permissions. Also do not forget adjusting `PLEROMA_HOST` and `PLEROMA_PORT`, if necessary.
```bash
cp pleroma_ejabberd_auth.py /etc/ejabberd/pleroma_ejabberd_auth.py
chown ejabberd /etc/ejabberd/pleroma_ejabberd_auth.py
chmod 700 /etc/ejabberd/pleroma_ejabberd_auth.py
```
Set external auth params in ejabberd.yaml file:
```bash
auth_method: [external]
extauth_program: "python3 /etc/ejabberd/pleroma_ejabberd_auth.py"
extauth_instances: 3
auth_use_cache: false
```
Restart / reload your ejabberd service.
After restarting your Ejabberd server, your users should now be able to connect with their Pleroma credentials.
```python
import sys
import struct
import http.client
from base64 import b64encode
import logging
PLEROMA_HOST = "127.0.0.1"
PLEROMA_PORT = "4000"
AUTH_ENDPOINT = "/api/v1/accounts/verify_credentials"
USER_ENDPOINT = "/api/v1/accounts"
LOGFILE = "/var/log/ejabberd/pleroma_auth.log"
logging.basicConfig(filename=LOGFILE, level=logging.INFO)
# Pleroma functions
def create_connection():
return http.client.HTTPConnection(PLEROMA_HOST, PLEROMA_PORT)
def verify_credentials(user: str, password: str) -> bool:
user_pass_b64 = b64encode("{}:{}".format(
user, password).encode('utf-8')).decode("ascii")
params = {}
headers = {
"Authorization": "Basic {}".format(user_pass_b64)
}
try:
conn = create_connection()
conn.request("GET", AUTH_ENDPOINT, params, headers)
response = conn.getresponse()
if response.status == 200:
return True
return False
except Exception as e:
logging.info("Can not connect: %s", str(e))
return False
def does_user_exist(user: str) -> bool:
conn = create_connection()
conn.request("GET", "{}/{}".format(USER_ENDPOINT, user))
response = conn.getresponse()
if response.status == 200:
return True
return False
def auth(username: str, server: str, password: str) -> bool:
return verify_credentials(username, password)
def isuser(username, server):
return does_user_exist(username)
def read():
(pkt_size,) = struct.unpack('>H', bytes(sys.stdin.read(2), encoding='utf8'))
pkt = sys.stdin.read(pkt_size)
cmd = pkt.split(':')[0]
if cmd == 'auth':
username, server, password = pkt.split(':', 3)[1:]
write(auth(username, server, password))
elif cmd == 'isuser':
username, server = pkt.split(':', 2)[1:]
write(isuser(username, server))
elif cmd == 'setpass':
# u, s, p = pkt.split(':', 3)[1:]
write(False)
elif cmd == 'tryregister':
# u, s, p = pkt.split(':', 3)[1:]
write(False)
elif cmd == 'removeuser':
# u, s = pkt.split(':', 2)[1:]
write(False)
elif cmd == 'removeuser3':
# u, s, p = pkt.split(':', 3)[1:]
write(False)
else:
write(False)
def write(result):
if result:
sys.stdout.write('\x00\x02\x00\x01')
else:
sys.stdout.write('\x00\x02\x00\x00')
sys.stdout.flush()
if __name__ == "__main__":
logging.info("Starting pleroma ejabberd auth daemon...")
while True:
try:
read()
except Exception as e:
logging.info(
"Error while processing data from ejabberd %s", str(e))
pass
```

View File

@ -40,7 +40,8 @@ defmodule Pleroma.Activity.Ir.Topics do
end end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
end end
defp item_creation_tags(tags, _, _) do defp item_creation_tags(tags, _, _) do
@ -55,9 +56,19 @@ defmodule Pleroma.Activity.Ir.Topics do
defp hashtags_to_topics(_), do: [] defp hashtags_to_topics(_), do: []
defp remote_topics(%{local: true}), do: []
defp remote_topics(%{actor: actor}) when is_binary(actor),
do: ["public:remote:" <> URI.parse(actor).host]
defp remote_topics(_), do: []
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"]
defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]
defp attachment_topics(_object, _act), do: ["public:media"] defp attachment_topics(_object, _act), do: ["public:media"]
end end

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Instances do
defdelegate reachable?(url_or_host), to: @adapter defdelegate reachable?(url_or_host), to: @adapter
defdelegate set_reachable(url_or_host), to: @adapter defdelegate set_reachable(url_or_host), to: @adapter
defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter
defdelegate get_consistently_unreachable(), to: @adapter
def set_consistently_unreachable(url_or_host), def set_consistently_unreachable(url_or_host),
do: set_unreachable(url_or_host, reachability_datetime_threshold()) do: set_unreachable(url_or_host, reachability_datetime_threshold())

View File

@ -119,6 +119,17 @@ defmodule Pleroma.Instances.Instance do
def set_unreachable(_, _), do: {:error, nil} def set_unreachable(_, _), do: {:error, nil}
def get_consistently_unreachable do
reachability_datetime_threshold = Instances.reachability_datetime_threshold()
from(i in Instance,
where: ^reachability_datetime_threshold > i.unreachable_since,
order_by: i.unreachable_since,
select: {i.host, i.unreachable_since}
)
|> Repo.all()
end
defp parse_datetime(datetime) when is_binary(datetime) do defp parse_datetime(datetime) when is_binary(datetime) do
NaiveDateTime.from_iso8601(datetime) NaiveDateTime.from_iso8601(datetime)
end end

View File

@ -23,7 +23,6 @@ defmodule Pleroma.Stats do
@impl true @impl true
def init(_args) do def init(_args) do
if Pleroma.Config.get(:env) == :test, do: :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
{:ok, nil, {:continue, :calculate_stats}} {:ok, nil, {:continue, :calculate_stats}}
end end
@ -32,11 +31,6 @@ defmodule Pleroma.Stats do
GenServer.call(__MODULE__, :force_update) GenServer.call(__MODULE__, :force_update)
end end
@doc "Performs collect stats"
def do_collect do
GenServer.cast(__MODULE__, :run_update)
end
@doc "Returns stats data" @doc "Returns stats data"
@spec get_stats() :: %{ @spec get_stats() :: %{
domain_count: non_neg_integer(), domain_count: non_neg_integer(),
@ -111,7 +105,11 @@ defmodule Pleroma.Stats do
@impl true @impl true
def handle_continue(:calculate_stats, _) do def handle_continue(:calculate_stats, _) do
stats = calculate_stat_data() stats = calculate_stat_data()
Process.send_after(self(), :run_update, @interval)
unless Pleroma.Config.get(:env) == :test do
Process.send_after(self(), :run_update, @interval)
end
{:noreply, stats} {:noreply, stats}
end end
@ -126,13 +124,6 @@ defmodule Pleroma.Stats do
{:reply, state, state} {:reply, state, state}
end end
@impl true
def handle_cast(:run_update, _state) do
new_stats = calculate_stat_data()
{:noreply, new_stats}
end
@impl true @impl true
def handle_info(:run_update, _) do def handle_info(:run_update, _) do
new_stats = calculate_stat_data() new_stats = calculate_stat_data()

View File

@ -1324,14 +1324,48 @@ defmodule Pleroma.User do
|> Repo.all() |> Repo.all()
end end
@spec mute(User.t(), User.t(), boolean()) :: @spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()} {:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
add_to_mutes(muter, mutee, notifications?) notifications? = Map.get(params, :notifications, true)
expires_in = Map.get(params, :expires_in, 0)
with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
{:ok, user_notification_mute} <-
(notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
{:ok, nil} do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_user",
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
schedule_in: expires_in
)
end
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
end end
def unmute(%User{} = muter, %User{} = mutee) do def unmute(%User{} = muter, %User{} = mutee) do
remove_from_mutes(muter, mutee) with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(muter, mutee) do
{:ok, [user_mute, user_notification_mute]}
end
end
def unmute(muter_id, mutee_id) do
with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
{:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
unmute(muter, mutee)
else
{who, result} = error ->
Logger.warn(
"User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
)
{:error, error}
end
end end
def subscribe(%User{} = subscriber, %User{} = target) do def subscribe(%User{} = subscriber, %User{} = target) do
@ -2320,23 +2354,6 @@ defmodule Pleroma.User do
UserRelationship.delete_block(user, blocked) UserRelationship.delete_block(user, blocked)
end end
defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
{:ok, user_notification_mute} <-
(notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
{:ok, nil} do
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
end
defp remove_from_mutes(user, %User{} = muted_user) do
with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(user, muted_user) do
{:ok, [user_mute, user_notification_mute]}
end
end
def set_invisible(user, invisible) do def set_invisible(user, invisible) do
params = %{invisible: invisible} params = %{invisible: invisible}

View File

@ -937,16 +937,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query defp restrict_muted_reblogs(query, _), do: query
defp restrict_instance(query, %{instance: instance}) do defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do
users = from(
from( activity in query,
u in User, where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance)
select: u.ap_id, )
where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
)
|> Repo.all()
from(activity in query, where: activity.actor in ^users)
end end
defp restrict_instance(query, _), do: query defp restrict_instance(query, _), do: query

View File

@ -262,6 +262,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query, :query,
%Schema{allOf: [BooleanLike], default: true}, %Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`." "Mute notifications in addition to statuses? Defaults to `true`."
),
Operation.parameter(
:expires_in,
:query,
%Schema{type: :integer, default: 0},
"Expire the mute in `expires_in` seconds. Default 0 for infinity"
) )
], ],
responses: %{ responses: %{
@ -723,10 +729,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true, nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.", description: "Mute notifications in addition to statuses? Defaults to true.",
default: true default: true
},
expires_in: %Schema{
type: :integer,
nullable: true,
description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
default: 0
} }
}, },
example: %{ example: %{
"notifications" => true "notifications" => true,
"expires_in" => 86_400
} }
} }
end end

View File

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Chat alias Pleroma.Web.ApiSpec.Schemas.Chat
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
@ -132,7 +133,10 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
tags: ["chat"], tags: ["chat"],
summary: "Get a list of chats that you participated in", summary: "Get a list of chats that you participated in",
operationId: "ChatController.index", operationId: "ChatController.index",
parameters: pagination_params(), parameters: [
Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
| pagination_params()
],
responses: %{ responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response()) 200 => Operation.response("The chats of the user", "application/json", chats_response())
}, },

View File

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaInstancesOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["PleromaInstances"],
summary: "Instances federation status",
description: "Information about instances deemed unreachable by the server",
operationId: "PleromaInstances.show",
responses: %{
200 => Operation.response("PleromaInstances", "application/json", pleroma_instances())
}
}
end
def pleroma_instances do
%Schema{
type: :object,
properties: %{
unreachable: %Schema{
type: :object,
properties: %{hostname: %Schema{type: :string, format: :"date-time"}}
}
},
example: %{
"unreachable" => %{"consistently-unreachable.name" => "2020-10-14 22:07:58.216473"}
}
}
end
end

View File

@ -223,7 +223,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
security: [%{"oAuth" => ["write:mutes"]}], security: [%{"oAuth" => ["write:mutes"]}],
description: "Do not receive notifications for the thread that this status is part of.", description: "Do not receive notifications for the thread that this status is part of.",
operationId: "StatusController.mute_conversation", operationId: "StatusController.mute_conversation",
parameters: [id_param()], requestBody:
request_body("Parameters", %Schema{
type: :object,
properties: %{
expires_in: %Schema{
type: :integer,
nullable: true,
description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
default: 0
}
}
}),
parameters: [
id_param(),
Operation.parameter(
:expires_in,
:query,
%Schema{type: :integer, default: 0},
"Expire the mute in `expires_in` seconds. Default 0 for infinity"
)
],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError) 400 => Operation.response("Error", "application/json", ApiError)

View File

@ -59,6 +59,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
parameters: [ parameters: [
local_param(), local_param(),
instance_param(),
only_media_param(), only_media_param(),
with_muted_param(), with_muted_param(),
exclude_visibilities_param(), exclude_visibilities_param(),
@ -158,8 +159,17 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
) )
end end
defp instance_param do
Operation.parameter(
:instance,
:query,
%Schema{type: :string},
"Show only statuses from the given domain"
)
end
defp with_muted_param do defp with_muted_param do
Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users") Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
end end
defp exclude_visibilities_param do defp exclude_visibilities_param do

View File

@ -28,8 +28,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
}, },
votes_count: %Schema{ votes_count: %Schema{
type: :integer, type: :integer,
nullable: true, description: "How many votes have been received. Number."
description: "How many votes have been received. Number, or null if `multiple` is false." },
voters_count: %Schema{
type: :integer,
description: "How many unique accounts have voted. Number."
}, },
voted: %Schema{ voted: %Schema{
type: :boolean, type: :boolean,
@ -61,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
expired: true, expired: true,
multiple: false, multiple: false,
votes_count: 10, votes_count: 10,
voters_count: nil, voters_count: 10,
voted: true, voted: true,
own_votes: [ own_votes: [
1 1

View File

@ -454,20 +454,46 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def add_mute(user, activity) do def add_mute(user, activity, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_conversation",
%{"user_id" => user.id, "activity_id" => activity.id},
schedule_in: expires_in
)
end
{:ok, activity} {:ok, activity}
else else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")} {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end end
end end
def remove_mute(user, activity) do def remove_mute(%User{} = user, %Activity{} = activity) do
ThreadMute.remove_mute(user.id, activity.data["context"]) ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity} {:ok, activity}
end end
def remove_mute(user_id, activity_id) do
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
remove_mute(user, activity)
else
{what, result} = error ->
Logger.warn(
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
activity_id
}"
)
{:error, error}
end
end
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
when is_binary(context) do when is_binary(context) do
ThreadMute.exists?(user_id, context) ThreadMute.exists?(user_id, context)

View File

@ -394,7 +394,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/mute" @doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted) render(conn, "relationship.json", user: muter, target: muted)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})

View File

@ -284,9 +284,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end end
@doc "POST /api/v1/statuses/:id/mute" @doc "POST /api/v1/statuses/:id/mute"
def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end end
end end

View File

@ -111,6 +111,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:blocking_user, user) |> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user) |> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user) |> Map.put(:reply_filtering_user, user)
|> Map.put(:instance, params[:instance])
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn

View File

@ -19,7 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired, expired: expired,
multiple: multiple, multiple: multiple,
votes_count: votes_count, votes_count: votes_count,
voters_count: (multiple || nil) && voters_count(object), voters_count: voters_count(object),
options: options, options: options,
voted: voted?(params), voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])

View File

@ -15,7 +15,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
alias Pleroma.Web.PleromaAPI.ChatView
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
import Ecto.Query import Ecto.Query
@ -121,9 +120,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
) do ) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id), with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
conn render(conn, "show.json", chat: chat)
|> put_view(ChatView)
|> render("show.json", chat: chat)
end end
end end
@ -141,33 +138,30 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end end
end end
def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do def index(%{assigns: %{user: %{id: user_id} = user}} = conn, params) do
blocked_ap_ids = User.blocked_users_ap_ids(user) exclude_users =
User.blocked_users_ap_ids(user) ++
if params[:with_muted], do: [], else: User.muted_users_ap_ids(user)
chats = chats =
Chat.for_user_query(user_id) user_id
|> where([c], c.recipient not in ^blocked_ap_ids) |> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users)
|> Repo.all() |> Repo.all()
conn render(conn, "index.json", chats: chats)
|> put_view(ChatView)
|> render("index.json", chats: chats)
end end
def create(%{assigns: %{user: user}} = conn, %{id: id}) do def create(%{assigns: %{user: user}} = conn, %{id: id}) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id), with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
conn render(conn, "show.json", chat: chat)
|> put_view(ChatView)
|> render("show.json", chat: chat)
end end
end end
def show(%{assigns: %{user: user}} = conn, %{id: id}) do def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
conn render(conn, "show.json", chat: chat)
|> put_view(ChatView)
|> render("show.json", chat: chat)
end end
end end

View File

@ -0,0 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.InstancesController do
use Pleroma.Web, :controller
alias Pleroma.Instances
plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaInstancesOperation
def show(conn, _params) do
unreachable =
Instances.get_consistently_unreachable()
|> Map.new(fn {host, date} -> {host, to_string(date)} end)
json(conn, %{"unreachable" => unreachable})
end
end

View File

@ -398,6 +398,7 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api) pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :index) get("/accounts/:id/scrobbles", ScrobbleController, :index)
get("/federation_status", InstancesController, :show)
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do

View File

@ -57,6 +57,15 @@ defmodule Pleroma.Web.Streamer do
{:ok, "hashtag:" <> tag} {:ok, "hashtag:" <> tag}
end end
# Allow remote instance streams.
def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do
{:ok, "public:remote:" <> instance}
end
def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do
{:ok, "public:remote:media:" <> instance}
end
# Expand user streams. # Expand user streams.
def get_topic( def get_topic(
stream, stream,

View File

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.MuteExpireWorker do
use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
@impl Oban.Worker
def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
Pleroma.User.unmute(muter_id, mutee_id)
:ok
end
def perform(%Job{
args: %{"op" => "unmute_conversation", "user_id" => user_id, "activity_id" => activity_id}
}) do
Pleroma.Web.CommonAPI.remove_mute(user_id, activity_id)
:ok
end
end

View File

@ -97,6 +97,20 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
refute Enum.member?(topics, "hashtag:2") refute Enum.member?(topics, "hashtag:2")
end end
test "non-local action produces public:remote topic", %{activity: activity} do
activity = %{activity | local: false, actor: "https://lain.com/users/lain"}
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:remote:lain.com")
end
test "local action doesn't produce public:remote topic", %{activity: activity} do
activity = %{activity | local: true, actor: "https://lain.com/users/lain"}
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public:remote:lain.com")
end
end end
describe "public visibility create events with attachments" do describe "public visibility create events with attachments" do
@ -128,6 +142,13 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
refute Enum.member?(topics, "public:local:media") refute Enum.member?(topics, "public:local:media")
end end
test "non-local action produces public:remote:media topic", %{activity: activity} do
activity = %{activity | local: false, actor: "https://lain.com/users/lain"}
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:remote:media:lain.com")
end
end end
describe "non-public visibility" do describe "non-public visibility" do

View File

@ -49,6 +49,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
test "allows public streams without authentication" do test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public") assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local") assert {:ok, _} = start_socket("?stream=public:local")
assert {:ok, _} = start_socket("?stream=public:remote&instance=lain.com")
assert {:ok, _} = start_socket("?stream=hashtag&tag=lain") assert {:ok, _} = start_socket("?stream=hashtag&tag=lain")
end end

View File

@ -229,7 +229,7 @@ defmodule Pleroma.NotificationTest do
muter = insert(:user) muter = insert(:user)
muted = insert(:user) muted = insert(:user)
{:ok, _user_relationships} = User.mute(muter, muted, false) {:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false})
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
@ -1015,7 +1015,7 @@ defmodule Pleroma.NotificationTest do
test "it returns notifications for muted user without notifications", %{user: user} do test "it returns notifications for muted user without notifications", %{user: user} do
muted = insert(:user) muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted, false) {:ok, _user_relationships} = User.mute(user, muted, %{notifications: false})
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})

View File

@ -1008,6 +1008,27 @@ defmodule Pleroma.UserTest do
assert User.muted_notifications?(user, muted_user) assert User.muted_notifications?(user, muted_user)
end end
test "expiring" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user, %{expires_in: 60})
assert User.mutes?(user, muted_user)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_user", "muter_id" => user.id, "mutee_id" => muted_user.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it unmutes users" do test "it unmutes users" do
user = insert(:user) user = insert(:user)
muted_user = insert(:user) muted_user = insert(:user)
@ -1019,6 +1040,17 @@ defmodule Pleroma.UserTest do
refute User.muted_notifications?(user, muted_user) refute User.muted_notifications?(user, muted_user)
end end
test "it unmutes users by id" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user)
{:ok, _user_mute} = User.unmute(user.id, muted_user.id)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it mutes user without notifications" do test "it mutes user without notifications" do
user = insert(:user) user = insert(:user)
muted_user = insert(:user) muted_user = insert(:user)
@ -1026,7 +1058,7 @@ defmodule Pleroma.UserTest do
refute User.mutes?(user, muted_user) refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user) refute User.muted_notifications?(user, muted_user)
{:ok, _user_relationships} = User.mute(user, muted_user, false) {:ok, _user_relationships} = User.mute(user, muted_user, %{notifications: false})
assert User.mutes?(user, muted_user) assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user) refute User.muted_notifications?(user, muted_user)

View File

@ -844,8 +844,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
describe "instances" do describe "instances" do
test "GET /instances/:instance/statuses", %{conn: conn} do test "GET /instances/:instance/statuses", %{conn: conn} do
user = insert(:user, local: false, nickname: "archaeme@archae.me") user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme")
user2 = insert(:user, local: false, nickname: "test@test.com") user2 = insert(:user, local: false, ap_id: "https://test.com/users/test")
insert_pair(:note_activity, user: user) insert_pair(:note_activity, user: user)
activity = insert(:note_activity, user: user2) activity = insert(:note_activity, user: user2)

View File

@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat alias Pleroma.Chat
@ -922,12 +922,34 @@ defmodule Pleroma.Web.CommonAPITest do
assert CommonAPI.thread_muted?(user, activity) assert CommonAPI.thread_muted?(user, activity)
end end
test "add expiring mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60})
assert CommonAPI.thread_muted?(user, activity)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute CommonAPI.thread_muted?(user, activity)
end
test "remove mute", %{user: user, activity: activity} do test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity) CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user, activity) {:ok, _} = CommonAPI.remove_mute(user, activity)
refute CommonAPI.thread_muted?(user, activity) refute CommonAPI.thread_muted?(user, activity)
end end
test "remove mute by ids", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user.id, activity.id)
refute CommonAPI.thread_muted?(user, activity)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity) CommonAPI.add_mute(user, activity)
{:error, _} = CommonAPI.add_mute(user, activity) {:error, _} = CommonAPI.add_mute(user, activity)

View File

@ -502,7 +502,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2, false) {:ok, _user_relationships} = User.mute(user, user2, %{notifications: false})
conn = get(conn, "/api/v1/notifications") conn = get(conn, "/api/v1/notifications")

View File

@ -147,6 +147,18 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
activities = json_response_and_validate_schema(res_conn, 200) activities = json_response_and_validate_schema(res_conn, 200)
[%{"id" => ^activity_id}] = activities [%{"id" => ^activity_id}] = activities
end end
test "can be filtered by instance", %{conn: conn} do
user = insert(:user, ap_id: "https://lain.com/users/lain")
insert(:note_activity, local: false)
insert(:note_activity, local: false)
{:ok, _} = CommonAPI.post(user, %{status: "test"})
conn = get(conn, "/api/v1/timelines/public?instance=lain.com")
assert length(json_response_and_validate_schema(conn, :ok)) == 1
end
end end
defp local_and_remote_activities do defp local_and_remote_activities do

View File

@ -277,7 +277,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user) {:ok, other_user} = User.follow(other_user, user)
{:ok, _subscription} = User.subscribe(user, other_user) {:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationships} = User.mute(user, other_user, true) {:ok, _user_relationships} = User.mute(user, other_user, %{notifications: true})
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
expected = expected =

View File

@ -44,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
], ],
voted: false, voted: false,
votes_count: 0, votes_count: 0,
voters_count: nil voters_count: 0
} }
result = PollView.render("show.json", %{object: object}) result = PollView.render("show.json", %{object: object})

View File

@ -343,6 +343,35 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
assert length(result) == 0 assert length(result) == 0
end end
test "it does not return chats with users you muted", %{conn: conn, user: user} do
recipient = insert(:user)
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
result =
conn
|> get("/api/v1/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 1
User.mute(user, recipient)
result =
conn
|> get("/api/v1/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 0
result =
conn
|> get("/api/v1/pleroma/chats?with_muted=true")
|> json_response_and_validate_schema(200)
assert length(result) == 1
end
test "it returns all chats", %{conn: conn, user: user} do test "it returns all chats", %{conn: conn, user: user} do
Enum.each(1..30, fn _ -> Enum.each(1..30, fn _ ->
recipient = insert(:user) recipient = insert(:user)

View File

@ -0,0 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Instances
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
setup do
constant = "http://consistently-unreachable.name/"
eventual = "http://eventually-unreachable.com/path"
{:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} =
Instances.set_consistently_unreachable(constant)
_eventual_unrechable = Instances.set_unreachable(eventual)
%{constant_unreachable: constant_unreachable, constant: constant}
end
test "GET /api/v1/pleroma/federation_status", %{
conn: conn,
constant_unreachable: constant_unreachable,
constant: constant
} do
constant_host = URI.parse(constant).host
assert conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/pleroma/federation_status")
|> json_response_and_validate_schema(200) == %{
"unreachable" => %{constant_host => to_string(constant_unreachable)}
}
end
end

View File

@ -29,6 +29,14 @@ defmodule Pleroma.Web.StreamerTest do
assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil)
end end
test "allows instance streams" do
assert {:ok, "public:remote:lain.com"} =
Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"})
assert {:ok, "public:remote:media:lain.com"} =
Streamer.get_topic("public:remote:media", nil, nil, %{"instance" => "lain.com"})
end
test "allows hashtag streams" do test "allows hashtag streams" do
assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"})
end end