diff --git a/config/config.exs b/config/config.exs
index 05b164273..a9539526d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -8,6 +8,10 @@ use Mix.Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
+config :pleroma, Pleroma.Repo,
+ types: Pleroma.PostgresTypes,
+ telemetry_event: [Pleroma.Repo.Instrumenter]
+
config :pleroma, Pleroma.Captcha,
enabled: false,
seconds_valid: 60,
@@ -87,6 +91,7 @@ websocket_config = [
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
+ instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
dispatch: [
@@ -356,7 +361,8 @@ config :pleroma, Pleroma.Web.Federator.RetryQueue,
config :pleroma_job_queue, :queues,
federator_incoming: 50,
federator_outgoing: 50,
- mailer: 10
+ mailer: 10,
+ scheduled_activities: 10
config :pleroma, :fetch_initial_posts,
enabled: false,
@@ -401,6 +407,13 @@ config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Sendmail
+config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
+
+config :pleroma, Pleroma.ScheduledActivity,
+ daily_user_limit: 25,
+ total_user_limit: 300,
+ enabled: true
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/test.exs b/config/test.exs
index 6a7b9067e..894fa8d3d 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -50,6 +50,11 @@ config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma_job_queue, disabled: true
+config :pleroma, Pleroma.ScheduledActivity,
+ daily_user_limit: 2,
+ total_user_limit: 3,
+ enabled: false
+
try do
import_config "test.secret.exs"
rescue
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 53b68ffd4..86cacebb1 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -58,6 +58,26 @@ Authentication is required and the user must be an admin.
- `password`
- Response: User’s nickname
+## `/api/pleroma/admin/user/follow`
+### Make a user follow another user
+
+- Methods: `POST`
+- Params:
+ - `follower`: The nickname of the follower
+ - `followed`: The nickname of the followed
+- Response:
+ - "ok"
+
+## `/api/pleroma/admin/user/unfollow`
+### Make a user unfollow another user
+
+- Methods: `POST`
+- Params:
+ - `follower`: The nickname of the follower
+ - `followed`: The nickname of the followed
+- Response:
+ - "ok"
+
## `/api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
diff --git a/docs/api/prometheus.md b/docs/api/prometheus.md
new file mode 100644
index 000000000..19c564e3c
--- /dev/null
+++ b/docs/api/prometheus.md
@@ -0,0 +1,22 @@
+# Prometheus Metrics
+
+Pleroma includes support for exporting metrics via the [prometheus_ex](https://github.com/deadtrickster/prometheus.ex) library.
+
+## `/api/pleroma/app_metrics`
+### Exports Prometheus application metrics
+* Method: `GET`
+* Authentication: not required
+* Params: none
+* Response: JSON
+
+## Grafana
+### Config example
+The following is a config example to use with [Grafana](https://grafana.com)
+
+```
+ - job_name: 'beam'
+ metrics_path: /api/pleroma/app_metrics
+ scheme: https
+ static_configs:
+ - targets: ['pleroma.soykaf.com']
+```
diff --git a/docs/config.md b/docs/config.md
index 686f1f36b..ed9b91363 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -218,14 +218,14 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
- `port`
* `url` - a list containing the configuration for generating urls, accepts
- `host` - the host without the scheme and a post (e.g `example.com`, not `https://example.com:2020`)
- - `scheme` - e.g `http`, `https`
+ - `scheme` - e.g `http`, `https`
- `port`
- `path`
**Important note**: if you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need
-Example:
+Example:
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "example.com", port: 2020, scheme: "https"],
@@ -317,6 +317,7 @@ Pleroma has the following queues:
* `federator_outgoing` - Outgoing federation
* `federator_incoming` - Incoming federation
* `mailer` - Email sender, see [`Pleroma.Mailer`](#pleroma-mailer)
+* `scheduled_activities` - Scheduled activities, see [`Pleroma.ScheduledActivities`](#pleromascheduledactivity)
Example:
@@ -390,6 +391,12 @@ config :auto_linker,
]
```
+## Pleroma.ScheduledActivity
+
+* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`)
+* `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`)
+* `enabled`: whether scheduled activities are sent to the job queue to be executed
+
## Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index bc3f8caba..ab8861b27 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.Activity do
field(:data, :map)
field(:local, :boolean, default: true)
field(:actor, :string)
- field(:recipients, {:array, :string})
+ field(:recipients, {:array, :string}, default: [])
has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 782d1d589..f0cb7d9a8 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -25,6 +25,7 @@ defmodule Pleroma.Application do
import Cachex.Spec
Pleroma.Config.DeprecationWarnings.warn()
+ setup_instrumenters()
# Define workers and child supervisors to be supervised
children =
@@ -103,7 +104,8 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
- worker(Pleroma.FlakeId, [])
+ worker(Pleroma.FlakeId, []),
+ worker(Pleroma.ScheduledActivityWorker, [])
] ++
hackney_pool_children() ++
[
@@ -126,6 +128,24 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
+ defp setup_instrumenters do
+ require Prometheus.Registry
+
+ :ok =
+ :telemetry.attach(
+ "prometheus-ecto",
+ [:pleroma, :repo, :query],
+ &Pleroma.Repo.Instrumenter.handle_event/4,
+ %{}
+ )
+
+ Prometheus.Registry.register_collector(:prometheus_process_collector)
+ Pleroma.Web.Endpoint.MetricsExporter.setup()
+ Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
+ Pleroma.Web.Endpoint.Instrumenter.setup()
+ Pleroma.Repo.Instrumenter.setup()
+ end
+
def enabled_hackney_pools do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 1e48749a8..7f1dbe28c 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -28,21 +28,20 @@ defmodule Pleroma.HTML do
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
- # TODO: rename object to activity because that's what it is really working with
- def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do
- key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}"
+ def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do
+ key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
- ensure_scrubbed_html(content, scrubbers, object.data["object"]["fake"] || false)
+ ensure_scrubbed_html(content, scrubbers, activity.data["object"]["fake"] || false)
end)
end
- def get_cached_stripped_html_for_object(content, object, module) do
- get_cached_scrubbed_html_for_object(
+ def get_cached_stripped_html_for_activity(content, activity, key) do
+ get_cached_scrubbed_html_for_activity(
content,
HtmlSanitizeEx.Scrubber.StripTags,
- object,
- module
+ activity,
+ key
)
end
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index 4af1bde56..aa5d427ae 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -8,6 +8,10 @@ defmodule Pleroma.Repo do
adapter: Ecto.Adapters.Postgres,
migration_timestamps: [type: :naive_datetime_usec]
+ defmodule Instrumenter do
+ use Prometheus.EctoInstrumenter
+ end
+
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
new file mode 100644
index 000000000..de0e54699
--- /dev/null
+++ b/lib/pleroma/scheduled_activity.ex
@@ -0,0 +1,161 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivity do
+ use Ecto.Schema
+
+ alias Pleroma.Config
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ @min_offset :timer.minutes(5)
+
+ schema "scheduled_activities" do
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ field(:scheduled_at, :naive_datetime)
+ field(:params, :map)
+
+ timestamps()
+ end
+
+ def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at, :params])
+ |> validate_required([:scheduled_at, :params])
+ |> validate_scheduled_at()
+ |> with_media_attachments()
+ end
+
+ defp with_media_attachments(
+ %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
+ )
+ when is_list(media_ids) do
+ media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
+
+ params =
+ params
+ |> Map.put("media_attachments", media_attachments)
+ |> Map.put("media_ids", media_ids)
+
+ put_change(changeset, :params, params)
+ end
+
+ defp with_media_attachments(changeset), do: changeset
+
+ def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at])
+ |> validate_required([:scheduled_at])
+ |> validate_scheduled_at()
+ end
+
+ def validate_scheduled_at(changeset) do
+ validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
+ cond do
+ not far_enough?(scheduled_at) ->
+ [scheduled_at: "must be at least 5 minutes from now"]
+
+ exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) ->
+ [scheduled_at: "daily limit exceeded"]
+
+ exceeds_total_user_limit?(changeset.data.user_id) ->
+ [scheduled_at: "total limit exceeded"]
+
+ true ->
+ []
+ end
+ end)
+ end
+
+ def exceeds_daily_user_limit?(user_id, scheduled_at) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
+ end
+
+ def exceeds_total_user_limit?(user_id) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
+ end
+
+ def far_enough?(scheduled_at) when is_binary(scheduled_at) do
+ with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do
+ far_enough?(scheduled_at)
+ else
+ _ -> false
+ end
+ end
+
+ def far_enough?(scheduled_at) do
+ now = NaiveDateTime.utc_now()
+ diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
+ diff > @min_offset
+ end
+
+ def new(%User{} = user, attrs) do
+ %ScheduledActivity{user_id: user.id}
+ |> changeset(attrs)
+ end
+
+ def create(%User{} = user, attrs) do
+ user
+ |> new(attrs)
+ |> Repo.insert()
+ end
+
+ def get(%User{} = user, scheduled_activity_id) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ |> where(id: ^scheduled_activity_id)
+ |> Repo.one()
+ end
+
+ def update(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> update_changeset(attrs)
+ |> Repo.update()
+ end
+
+ def delete(%ScheduledActivity{} = scheduled_activity) do
+ scheduled_activity
+ |> Repo.delete()
+ end
+
+ def delete(id) when is_binary(id) or is_integer(id) do
+ ScheduledActivity
+ |> where(id: ^id)
+ |> select([sa], sa)
+ |> Repo.delete_all()
+ |> case do
+ {1, [scheduled_activity]} -> {:ok, scheduled_activity}
+ _ -> :error
+ end
+ end
+
+ def for_user_query(%User{} = user) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ end
+
+ def due_activities(offset \\ 0) do
+ naive_datetime =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(offset, :millisecond)
+
+ ScheduledActivity
+ |> where([sa], sa.scheduled_at < ^naive_datetime)
+ |> Repo.all()
+ end
+end
diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex
new file mode 100644
index 000000000..65b38622f
--- /dev/null
+++ b/lib/pleroma/scheduled_activity_worker.ex
@@ -0,0 +1,58 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivityWorker do
+ @moduledoc """
+ Sends scheduled activities to the job queue.
+ """
+
+ alias Pleroma.Config
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ use GenServer
+ require Logger
+
+ @schedule_interval :timer.minutes(1)
+
+ def start_link do
+ GenServer.start_link(__MODULE__, nil)
+ end
+
+ def init(_) do
+ if Config.get([ScheduledActivity, :enabled]) do
+ schedule_next()
+ {:ok, nil}
+ else
+ :ignore
+ end
+ end
+
+ def perform(:execute, scheduled_activity_id) do
+ try do
+ {:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id)
+ %User{} = user = User.get_cached_by_id(scheduled_activity.user_id)
+ {:ok, _result} = CommonAPI.post(user, scheduled_activity.params)
+ rescue
+ error ->
+ Logger.error(
+ "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}"
+ )
+ end
+ end
+
+ def handle_info(:perform, state) do
+ ScheduledActivity.due_activities(@schedule_interval)
+ |> Enum.each(fn scheduled_activity ->
+ PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id])
+ end)
+
+ schedule_next()
+ {:noreply, state}
+ end
+
+ defp schedule_next do
+ Process.send_after(self(), :perform, @schedule_interval)
+ end
+end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index b3a09e49e..78bf31893 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -25,6 +25,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(nickname)
end
+ def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
+ with %User{} = follower <- User.get_by_nickname(follower_nick),
+ %User{} = followed <- User.get_by_nickname(followed_nick) do
+ User.follow(follower, followed)
+ end
+
+ conn
+ |> json("ok")
+ end
+
+ def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
+ with %User{} = follower <- User.get_by_nickname(follower_nick),
+ %User{} = followed <- User.get_by_nickname(followed_nick) do
+ User.unfollow(follower, followed)
+ end
+
+ conn
+ |> json("ok")
+ end
+
def user_create(
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 085f23159..1633477c3 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -80,6 +80,26 @@ defmodule Pleroma.Web.Endpoint do
extra: same_site
)
+ # Note: the plug and its configuration is compile-time this can't be upstreamed yet
+ if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
+ plug(RemoteIp, proxies: proxies)
+ end
+
+ defmodule Instrumenter do
+ use Prometheus.PhoenixInstrumenter
+ end
+
+ defmodule PipelineInstrumenter do
+ use Prometheus.PlugPipelineInstrumenter
+ end
+
+ defmodule MetricsExporter do
+ use Prometheus.PlugExporter
+ end
+
+ plug(PipelineInstrumenter)
+ plug(MetricsExporter)
+
plug(Pleroma.Web.Router)
@doc """
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 08ea5f967..382f07e6b 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
+ alias Pleroma.ScheduledActivity
alias Pleroma.User
def get_followers(user, params \\ %{}) do
@@ -28,6 +29,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|> Pagination.fetch_paginated(params)
end
+ def get_scheduled_activities(user, params \\ %{}) do
+ user
+ |> ScheduledActivity.for_user_query()
+ |> Pagination.fetch_paginated(params)
+ end
+
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string}
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 89fd7629a..fc8a2458c 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -5,12 +5,14 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
+ alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@@ -25,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
@@ -364,6 +367,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
+ with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+ conn
+ |> add_link_headers(:scheduled_statuses, scheduled_activities)
+ |> put_view(ScheduledActivityView)
+ |> render("index.json", %{scheduled_activities: scheduled_activities})
+ end
+ end
+
+ def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def update_scheduled_status(
+ %{assigns: %{user: user}} = conn,
+ %{"id" => scheduled_activity_id} = params
+ ) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
@@ -384,12 +436,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> Ecto.UUID.generate()
end
- {:ok, activity} =
- Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
+ scheduled_at = params["scheduled_at"]
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
+ with {:ok, scheduled_activity} <-
+ ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ end
+ else
+ params = Map.drop(params, ["scheduled_at"])
+
+ {:ok, activity} =
+ Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
+ CommonAPI.post(user, params)
+ end)
+
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@@ -1091,9 +1158,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def index(%{assigns: %{user: user}} = conn, _params) do
- token =
- conn
- |> get_session(:oauth_token)
+ token = get_session(conn, :oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
@@ -1194,6 +1259,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render("index.html", %{initial_state: initial_state, flavour: flavour})
else
conn
+ |> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
@@ -1278,12 +1344,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
scope: Enum.join(app.scopes, " ")
)
- conn
- |> redirect(to: path)
+ redirect(conn, to: path)
end
end
- defp local_mastodon_root_path(conn), do: mastodon_api_path(conn, :index, ["getting-started"])
+ defp local_mastodon_root_path(conn) do
+ case get_session(conn, :return_to) do
+ nil ->
+ mastodon_api_path(conn, :index, ["getting-started"])
+
+ return_to ->
+ delete_session(conn, :return_to)
+ return_to
+ end
+ end
defp get_or_make_app do
find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
@@ -1399,6 +1473,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# fallback action
#
+ def errors(conn, {:error, %Changeset{} = changeset}) do
+ error_message =
+ changeset
+ |> Changeset.traverse_errors(fn {message, _opt} -> message end)
+ |> Enum.map_join(", ", fn {_k, v} -> v end)
+
+ conn
+ |> put_status(422)
+ |> json(%{error: error_message})
+ end
+
+ def errors(conn, {:error, :not_found}) do
+ conn
+ |> put_status(404)
+ |> json(%{error: "Record not found"})
+ end
+
def errors(conn, _) do
conn
|> put_status(500)
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
new file mode 100644
index 000000000..0aae15ab9
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("index.json", %{scheduled_activities: scheduled_activities}) do
+ render_many(scheduled_activities, ScheduledActivityView, "show.json")
+ end
+
+ def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
+ %{
+ id: to_string(scheduled_activity.id),
+ scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at),
+ params: status_params(scheduled_activity.params)
+ }
+ |> with_media_attachments(scheduled_activity)
+ end
+
+ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
+ try do
+ attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
+ Map.put(data, :media_attachments, attachments)
+ rescue
+ _ -> data
+ end
+ end
+
+ defp with_media_attachments(data, _), do: data
+
+ defp status_params(params) do
+ data = %{
+ text: params["status"],
+ sensitive: params["sensitive"],
+ spoiler_text: params["spoiler_text"],
+ visibility: params["visibility"],
+ scheduled_at: params["scheduled_at"],
+ poll: params["poll"],
+ in_reply_to_id: params["in_reply_to_id"]
+ }
+
+ data =
+ if media_ids = params["media_ids"] do
+ Map.put(data, :media_ids, media_ids)
+ else
+ data
+ end
+
+ data
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 200bb453d..4c0b53bdd 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -147,10 +147,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
content =
object
|> render_content()
- |> HTML.get_cached_scrubbed_html_for_object(
+ |> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
- __MODULE__
+ "mastoapi:content"
+ )
+
+ summary =
+ (object["summary"] || "")
+ |> HTML.get_cached_scrubbed_html_for_activity(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ "mastoapi:summary"
)
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
@@ -182,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
- spoiler_text: object["summary"] || "",
+ spoiler_text: summary,
visibility: get_visibility(object),
media_attachments: attachments,
mentions: mentions,
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 23bbde1a6..58385a3d1 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.Metadata.Utils do
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/
/, " ")
- |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
+ |> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Formatter.demojify()
|> Formatter.truncate()
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 108303eb2..bee7084ad 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -154,6 +154,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)},
%App{} = app <- get_app_from_request(conn, params),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
+ {:user_active, true} <- {:user_active, !user.info.deactivated},
scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes,
true <- Enum.any?(scopes),
@@ -177,6 +178,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|> put_status(:forbidden)
|> json(%{error: "Your login is missing a confirmed e-mail address"})
+ {:user_active, false} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{error: "Your account is currently disabled"})
+
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5101e1b18..ef38fc34d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -145,8 +145,12 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :oauth_write])
+ post("/user/follow", AdminAPIController, :user_follow)
+ post("/user/unfollow", AdminAPIController, :user_unfollow)
+
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
+
delete("/user", AdminAPIController, :user_delete)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
post("/user", AdminAPIController, :user_create)
@@ -255,6 +259,9 @@ defmodule Pleroma.Web.Router do
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
+ get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
+ get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
+
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
@@ -289,6 +296,9 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
+ put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
+ delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
+
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index aa1d41fa2..433322eb8 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -254,10 +254,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
html =
content
- |> HTML.get_cached_scrubbed_html_for_object(
+ |> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
- __MODULE__
+ "twitterapi:content"
)
|> Formatter.emojify(object["emoji"])
@@ -265,7 +265,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
if content do
content
|> String.replace(~r/
/, "\n")
- |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
+ |> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")
else
""
end
diff --git a/mix.exs b/mix.exs
index 30931821f..26a03b70b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -103,6 +103,13 @@ defmodule Pleroma.Mixfile do
git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "479dd343f4e563ff91215c8275f3b5c67e032850"},
{:pleroma_job_queue, "~> 0.2.0"},
+ {:telemetry, "~> 0.3"},
+ {:prometheus_ex, "~> 3.0"},
+ {:prometheus_plugs, "~> 1.1"},
+ {:prometheus_phoenix, "~> 1.2"},
+ {:prometheus_ecto, "~> 1.4"},
+ {:prometheus_process_collector, "~> 1.4"},
+ {:recon, github: "ferd/recon", tag: "2.4.0"},
{:quack, "~> 0.1.1"}
] ++ oauth_deps
end
diff --git a/mix.lock b/mix.lock
index eb1d1221e..bb40ebd48 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,4 +1,5 @@
%{
+ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"},
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "479dd343f4e563ff91215c8275f3b5c67e032850", [ref: "479dd343f4e563ff91215c8275f3b5c67e032850"]},
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
@@ -57,8 +58,15 @@
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
+ "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"},
+ "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
+ "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
+ "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
+ "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"},
+ "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
"quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
+ "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
diff --git a/priv/repo/migrations/20190328053912_create_scheduled_activities.exs b/priv/repo/migrations/20190328053912_create_scheduled_activities.exs
new file mode 100644
index 000000000..dd737e25a
--- /dev/null
+++ b/priv/repo/migrations/20190328053912_create_scheduled_activities.exs
@@ -0,0 +1,16 @@
+defmodule Pleroma.Repo.Migrations.CreateScheduledActivities do
+ use Ecto.Migration
+
+ def change do
+ create table(:scheduled_activities) do
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:scheduled_at, :naive_datetime, null: false)
+ add(:params, :map, null: false)
+
+ timestamps()
+ end
+
+ create(index(:scheduled_activities, [:scheduled_at]))
+ create(index(:scheduled_activities, [:user_id]))
+ end
+end
diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs
index 302662797..8b0b06772 100644
--- a/test/plugs/legacy_authentication_plug_test.exs
+++ b/test/plugs/legacy_authentication_plug_test.exs
@@ -47,16 +47,18 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
|> assign(:auth_user, user)
conn =
- with_mock User,
- reset_password: fn user, %{password: password, password_confirmation: password} ->
- send(self(), :reset_password)
- {:ok, user}
- end do
- conn
- |> LegacyAuthenticationPlug.call(%{})
+ with_mocks([
+ {:crypt, [], [crypt: fn _password, password_hash -> password_hash end]},
+ {User, [],
+ [
+ reset_password: fn user, %{password: password, password_confirmation: password} ->
+ {:ok, user}
+ end
+ ]}
+ ]) do
+ LegacyAuthenticationPlug.call(conn, %{})
end
- assert_received :reset_password
assert conn.assigns.user == user
end
diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs
new file mode 100644
index 000000000..edc7cc3f9
--- /dev/null
+++ b/test/scheduled_activity_test.exs
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivityTest do
+ use Pleroma.DataCase
+ alias Pleroma.DataCase
+ alias Pleroma.ScheduledActivity
+ import Pleroma.Factory
+
+ setup context do
+ DataCase.ensure_local_uploader(context)
+ end
+
+ describe "creation" do
+ test "when daily user limit is exceeded" do
+ user = insert(:user)
+
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:error, changeset} = ScheduledActivity.create(user, attrs)
+ assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}]
+ end
+
+ test "when total user limit is exceeded" do
+ user = insert(:user)
+
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ tomorrow =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.hours(36), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today})
+ {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today})
+ {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
+ {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
+ assert changeset.errors == [scheduled_at: {"total limit exceeded", []}]
+ end
+
+ test "when scheduled_at is earlier than 5 minute from now" do
+ user = insert(:user)
+
+ scheduled_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(4), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: scheduled_at}
+ {:error, changeset} = ScheduledActivity.create(user, attrs)
+ assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}]
+ end
+ end
+end
diff --git a/test/scheduled_activity_worker_test.exs b/test/scheduled_activity_worker_test.exs
new file mode 100644
index 000000000..b9c91dda6
--- /dev/null
+++ b/test/scheduled_activity_worker_test.exs
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivityWorkerTest do
+ use Pleroma.DataCase
+ alias Pleroma.ScheduledActivity
+ import Pleroma.Factory
+
+ test "creates a status from the scheduled activity" do
+ user = insert(:user)
+ scheduled_activity = insert(:scheduled_activity, user: user, params: %{status: "hi"})
+ Pleroma.ScheduledActivityWorker.perform(:execute, scheduled_activity.id)
+
+ refute Repo.get(ScheduledActivity, scheduled_activity.id)
+ activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id))
+ assert activity.data["object"]["content"] == "hi"
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 67953931b..ea59912cf 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -240,6 +240,16 @@ defmodule Pleroma.Factory do
}
end
+ def oauth_authorization_factory do
+ %Pleroma.Web.OAuth.Authorization{
+ token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
+ scopes: ["read", "write", "follow", "push"],
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
+ user: build(:user),
+ app: build(:oauth_app)
+ }
+ end
+
def push_subscription_factory do
%Pleroma.Web.Push.Subscription{
user: build(:user),
@@ -258,6 +268,14 @@ defmodule Pleroma.Factory do
}
end
+ def scheduled_activity_factory do
+ %Pleroma.ScheduledActivity{
+ user: build(:user),
+ scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
+ params: build(:note) |> Map.from_struct() |> Map.get(:data)
+ }
+ end
+
def registration_factory do
user = insert(:user)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index acae64361..dd2fbfb15 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -74,6 +74,52 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
+ describe "/api/pleroma/admin/user/follow" do
+ test "allows to force-follow another user" do
+ admin = insert(:user, info: %{is_admin: true})
+ user = insert(:user)
+ follower = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/user/follow", %{
+ "follower" => follower.nickname,
+ "followed" => user.nickname
+ })
+
+ user = User.get_by_id(user.id)
+ follower = User.get_by_id(follower.id)
+
+ assert User.following?(follower, user)
+ end
+ end
+
+ describe "/api/pleroma/admin/user/unfollow" do
+ test "allows to force-unfollow another user" do
+ admin = insert(:user, info: %{is_admin: true})
+ user = insert(:user)
+ follower = insert(:user)
+
+ User.follow(follower, user)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/user/unfollow", %{
+ "follower" => follower.nickname,
+ "followed" => user.nickname
+ })
+
+ user = User.get_by_id(user.id)
+ follower = User.get_by_id(follower.id)
+
+ refute User.following?(follower, user)
+ end
+ end
+
describe "PUT /api/pleroma/admin/users/tag" do
setup do
admin = insert(:user, info: %{is_admin: true})
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 6060cc97f..cd01116e2 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
@@ -2340,4 +2341,281 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
refute acc_one == acc_two
assert acc_two == acc_three
end
+
+ describe "index/2 redirections" do
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ test_path = "/web/statuses/test"
+ %{conn: conn, path: test_path}
+ end
+
+ test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
+ conn = get(conn, path)
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/web/login"
+ end
+
+ test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
+ token = insert(:oauth_token)
+
+ conn =
+ conn
+ |> assign(:user, token.user)
+ |> put_session(:oauth_token, token.token)
+ |> get(path)
+
+ assert conn.status == 200
+ end
+
+ test "saves referer path to session", %{conn: conn, path: path} do
+ conn = get(conn, path)
+ return_to = Plug.Conn.get_session(conn, :return_to)
+
+ assert return_to == path
+ end
+
+ test "redirects to the saved path after log in", %{conn: conn, path: path} do
+ app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
+ auth = insert(:oauth_authorization, app: app)
+
+ conn =
+ conn
+ |> put_session(:return_to, path)
+ |> get("/web/login", %{code: auth.token})
+
+ assert conn.status == 302
+ assert redirected_to(conn) == path
+ end
+
+ test "redirects to the getting-started page when referer is not present", %{conn: conn} do
+ app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
+ auth = insert(:oauth_authorization, app: app)
+
+ conn = get(conn, "/web/login", %{code: auth.token})
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/web/getting-started"
+ end
+ end
+
+ describe "scheduled activities" do
+ test "creates a scheduled activity", %{conn: conn} do
+ user = insert(:user)
+ scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses", %{
+ "status" => "scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
+ assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(scheduled_at)
+ assert [] == Repo.all(Activity)
+ end
+
+ test "creates a scheduled activity with a media attachment", %{conn: conn} do
+ user = insert(:user)
+ scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses", %{
+ "media_ids" => [to_string(upload.id)],
+ "status" => "scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
+ assert %{"type" => "image"} = media_attachment
+ end
+
+ test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
+ %{conn: conn} do
+ user = insert(:user)
+
+ scheduled_at =
+ NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses", %{
+ "status" => "not scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"content" => "not scheduled"} = json_response(conn, 200)
+ assert [] == Repo.all(ScheduledActivity)
+ end
+
+ test "returns error when daily user limit is exceeded", %{conn: conn} do
+ user = insert(:user)
+
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
+
+ assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
+ end
+
+ test "returns error when total user limit is exceeded", %{conn: conn} do
+ user = insert(:user)
+
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ tomorrow =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.hours(36), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
+
+ assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
+ end
+
+ test "shows scheduled activities", %{conn: conn} do
+ user = insert(:user)
+ scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string()
+
+ conn =
+ conn
+ |> assign(:user, user)
+
+ # min_id
+ conn_res =
+ conn
+ |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
+
+ # since_id
+ conn_res =
+ conn
+ |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
+
+ # max_id
+ conn_res =
+ conn
+ |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
+ end
+
+ test "shows a scheduled activity", %{conn: conn} do
+ user = insert(:user)
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
+ assert scheduled_activity_id == scheduled_activity.id |> to_string()
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/scheduled_statuses/404")
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+
+ test "updates a scheduled activity", %{conn: conn} do
+ user = insert(:user)
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ new_scheduled_at =
+ NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+ scheduled_at: new_scheduled_at
+ })
+
+ assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
+ assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+
+ test "deletes a scheduled activity", %{conn: conn} do
+ user = insert(:user)
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{} = json_response(res_conn, 200)
+ assert nil == Repo.get(ScheduledActivity, scheduled_activity.id)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+ end
end
diff --git a/test/web/mastodon_api/scheduled_activity_view_test.exs b/test/web/mastodon_api/scheduled_activity_view_test.exs
new file mode 100644
index 000000000..ecbb855d4
--- /dev/null
+++ b/test/web/mastodon_api/scheduled_activity_view_test.exs
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
+ use Pleroma.DataCase
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ alias Pleroma.Web.MastodonAPI.StatusView
+ import Pleroma.Factory
+
+ test "A scheduled activity with a media attachment" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hi"})
+
+ scheduled_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(10), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+ attrs = %{
+ params: %{
+ "media_ids" => [upload.id],
+ "status" => "hi",
+ "sensitive" => true,
+ "spoiler_text" => "spoiler",
+ "visibility" => "unlisted",
+ "in_reply_to_id" => to_string(activity.id)
+ },
+ scheduled_at: scheduled_at
+ }
+
+ {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs)
+ result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity})
+
+ expected = %{
+ id: to_string(scheduled_activity.id),
+ media_attachments:
+ %{"media_ids" => [upload.id]}
+ |> Utils.attachments_from_ids()
+ |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})),
+ params: %{
+ in_reply_to_id: to_string(activity.id),
+ media_ids: [upload.id],
+ poll: nil,
+ scheduled_at: nil,
+ sensitive: true,
+ spoiler_text: "spoiler",
+ text: "hi",
+ visibility: "unlisted"
+ },
+ scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at)
+ }
+
+ assert expected == result
+ end
+end
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index 385896dc6..ac7843f9b 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -633,6 +633,32 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
refute Map.has_key?(resp, "access_token")
end
+ test "rejects token exchange for valid credentials belonging to deactivated user" do
+ password = "testpassword"
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ info: %{deactivated: true}
+ )
+
+ app = insert(:oauth_app)
+
+ conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ assert resp = json_response(conn, 403)
+ assert %{"error" => _} = resp
+ refute Map.has_key?(resp, "access_token")
+ end
+
test "rejects an invalid authorization code" do
app = insert(:oauth_app)