diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pleroma/activity.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/activity_expiration.ex | 68 | ||||
-rw-r--r-- | lib/pleroma/activity_expiration_worker.ex | 62 | ||||
-rw-r--r-- | lib/pleroma/application.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/moderation_log.ex | 433 | ||||
-rw-r--r-- | lib/pleroma/user.ex | 8 | ||||
-rw-r--r-- | lib/pleroma/user/info.ex | 6 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/admin_api_controller.ex | 241 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/views/account_view.ex | 46 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/views/moderation_log_view.ex | 26 | ||||
-rw-r--r-- | lib/pleroma/web/common_api/common_api.ex | 46 | ||||
-rw-r--r-- | lib/pleroma/web/common_api/utils.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/views/status_view.ex | 11 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/ostatus_controller.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 4 |
15 files changed, 908 insertions, 55 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 35612c882..2d4e9da0c 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Activity do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object @@ -59,6 +60,8 @@ defmodule Pleroma.Activity do # typical case. has_one(:object, Object, on_delete: :nothing, foreign_key: :id) + has_one(:expiration, ActivityExpiration, on_delete: :delete_all) + timestamps() end diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex new file mode 100644 index 000000000..bf57abca4 --- /dev/null +++ b/lib/pleroma/activity_expiration.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpiration do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.FlakeId + alias Pleroma.Repo + + import Ecto.Changeset + import Ecto.Query + + @type t :: %__MODULE__{} + @min_activity_lifetime :timer.hours(1) + + schema "activity_expirations" do + belongs_to(:activity, Activity, type: FlakeId) + field(:scheduled_at, :naive_datetime) + end + + def changeset(%ActivityExpiration{} = expiration, attrs) do + expiration + |> cast(attrs, [:scheduled_at]) + |> validate_required([:scheduled_at]) + |> validate_scheduled_at() + end + + def get_by_activity_id(activity_id) do + ActivityExpiration + |> where([exp], exp.activity_id == ^activity_id) + |> Repo.one() + end + + def create(%Activity{} = activity, scheduled_at) do + %ActivityExpiration{activity_id: activity.id} + |> changeset(%{scheduled_at: scheduled_at}) + |> Repo.insert() + end + + def due_expirations(offset \\ 0) do + naive_datetime = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(offset, :millisecond) + + ActivityExpiration + |> where([exp], exp.scheduled_at < ^naive_datetime) + |> Repo.all() + end + + def validate_scheduled_at(changeset) do + validate_change(changeset, :scheduled_at, fn _, scheduled_at -> + if not expires_late_enough?(scheduled_at) do + [scheduled_at: "an ephemeral activity must live for at least one hour"] + else + [] + end + end) + end + + def expires_late_enough?(scheduled_at) do + now = NaiveDateTime.utc_now() + diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) + diff >= @min_activity_lifetime + end +end diff --git a/lib/pleroma/activity_expiration_worker.ex b/lib/pleroma/activity_expiration_worker.ex new file mode 100644 index 000000000..0f9e715f8 --- /dev/null +++ b/lib/pleroma/activity_expiration_worker.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationWorker do + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + require Logger + use GenServer + import Ecto.Query + + @schedule_interval :timer.minutes(1) + + def start_link(_) do + GenServer.start_link(__MODULE__, nil) + end + + @impl true + def init(_) do + if Config.get([ActivityExpiration, :enabled]) do + schedule_next() + {:ok, nil} + else + :ignore + end + end + + def perform(:execute, expiration_id) do + try do + expiration = + ActivityExpiration + |> where([e], e.id == ^expiration_id) + |> Repo.one!() + + activity = Activity.get_by_id_with_object(expiration.activity_id) + user = User.get_by_ap_id(activity.object.data["actor"]) + CommonAPI.delete(activity.id, user) + rescue + error -> + Logger.error("#{__MODULE__} Couldn't delete expired activity: #{inspect(error)}") + end + end + + @impl true + def handle_info(:perform, state) do + ActivityExpiration.due_expirations(@schedule_interval) + |> Enum.each(fn expiration -> + PleromaJobQueue.enqueue(:activity_expiration, __MODULE__, [:execute, expiration.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/application.ex b/lib/pleroma/application.ex index 25e56b9e2..483ac1f39 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -35,7 +35,8 @@ defmodule Pleroma.Application do Pleroma.Emoji, Pleroma.Captcha, Pleroma.FlakeId, - Pleroma.ScheduledActivityWorker + Pleroma.ScheduledActivityWorker, + Pleroma.ActivityExpirationWorker ] ++ cachex_children() ++ hackney_pool_children() ++ diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex new file mode 100644 index 000000000..1ef6fe67a --- /dev/null +++ b/lib/pleroma/moderation_log.ex @@ -0,0 +1,433 @@ +defmodule Pleroma.ModerationLog do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Query + + schema "moderation_log" do + field(:data, :map) + + timestamps() + end + + def get_all(page, page_size) do + from(q in __MODULE__, + order_by: [desc: q.inserted_at], + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + |> Repo.all() + end + + def insert_log(%{ + actor: %User{} = actor, + subject: %User{} = subject, + action: action, + permission: permission + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + subject: user_to_map(subject), + action: action, + permission: permission + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_update", + subject: %Activity{data: %{"type" => "Flag"}} = subject + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_update", + subject: report_to_map(subject) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_response", + subject: %Activity{} = subject, + text: text + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_response", + subject: report_to_map(subject), + text: text + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_update", + subject: %Activity{} = subject, + sensitive: sensitive, + visibility: visibility + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_update", + subject: status_to_map(subject), + sensitive: sensitive, + visibility: visibility + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_delete", + subject_id: subject_id + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_delete", + subject_id: subject_id + } + }) + end + + @spec insert_log(%{actor: User, subject: User, action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subject: user_to_map(subject) + } + }) + end + + @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do + subjects = Enum.map(subjects, &user_to_map/1) + + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subjects: subjects + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "follow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "follow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "unfollow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "unfollow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + nicknames: nicknames, + tags: tags, + action: action + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + nicknames: nicknames, + tags: tags, + action: action + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: action, + target: target + }) + when action in ["relay_follow", "relay_unfollow"] do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + target: target + } + }) + end + + defp user_to_map(%User{} = user) do + user + |> Map.from_struct() + |> Map.take([:id, :nickname]) + |> Map.put(:type, "user") + end + + defp report_to_map(%Activity{} = report) do + %{ + type: "report", + id: report.id, + state: report.data["state"] + } + end + + defp status_to_map(%Activity{} = status) do + %{ + type: "status", + id: status.id + } + end + + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => action, + "followed" => %{"nickname" => followed_nickname}, + "follower" => %{"nickname" => follower_nickname} + } + }) do + "@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "delete", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deleted user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "create", + "subjects" => subjects + } + }) do + nicknames = + subjects + |> Enum.map(&"@#{&1["nickname"]}") + |> Enum.join(", ") + + "@#{actor_nickname} created users: #{nicknames}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "activate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} activated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "deactivate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deactivated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "action" => "tag" + } + }) do + nicknames_string = + nicknames + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags_string = tags |> Enum.join(", ") + + "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "action" => "untag" + } + }) do + nicknames_string = + nicknames + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags_string = tags |> Enum.join(", ") + + "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "grant", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} made @#{subject_nickname} #{permission}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "revoke", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_follow", + "target" => target + } + }) do + "@#{actor_nickname} followed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_unfollow", + "target" => target + } + }) do + "@#{actor_nickname} unfollowed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_update", + "subject" => %{"id" => subject_id, "state" => state, "type" => "report"} + } + }) do + "@#{actor_nickname} updated report ##{subject_id} with '#{state}' state" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_response", + "subject" => %{"id" => subject_id, "type" => "report"}, + "text" => text + } + }) do + "@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => nil, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => nil + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{ + visibility + }'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_delete", + "subject_id" => subject_id + } + }) do + "@#{actor_nickname} deleted status ##{subject_id}" + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 134b8bb6c..29fd6d2ea 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -330,7 +330,13 @@ defmodule Pleroma.User do @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" def register(%Ecto.Changeset{} = changeset) do with {:ok, user} <- Repo.insert(changeset), - {:ok, user} <- autofollow_users(user), + {:ok, user} <- post_register_action(user) do + {:ok, user} + end + end + + def post_register_action(%User{} = user) do + with {:ok, user} <- autofollow_users(user), {:ok, user} <- set_cache(user), {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- try_send_confirmation_email(user) do diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 45a39924b..779bfbc18 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -49,7 +49,7 @@ defmodule Pleroma.User.Info do field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) field(:pleroma_settings_store, :map, default: %{}) - field(:fields, {:array, :map}, default: []) + field(:fields, {:array, :map}, default: nil) field(:raw_fields, {:array, :map}, default: []) field(:notification_settings, :map, @@ -422,7 +422,7 @@ defmodule Pleroma.User.Info do # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. # For example: [{"name": "Pronoun", "value": "she/her"}, …] - def fields(%{fields: [], source_data: %{"attachment" => attachment}}) do + def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) attachment @@ -431,6 +431,8 @@ defmodule Pleroma.User.Info do |> Enum.take(limit) end + def fields(%{fields: nil}), do: [] + def fields(%{fields: fields}), do: fields def follow_information_update(info, params) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2d3d0adc4..544b9d7d8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller alias Pleroma.Activity + alias Pleroma.ModerationLog alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub @@ -12,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.ConfigView + alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -25,52 +27,113 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do action_fallback(:errors) - def user_delete(conn, %{"nickname" => nickname}) do - User.get_cached_by_nickname(nickname) - |> User.delete() + def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + user = User.get_cached_by_nickname(nickname) + User.delete(user) + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: "delete" + }) conn |> json(nickname) end - def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_follow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.follow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "follow" + }) end conn |> json("ok") end - def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_unfollow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.unfollow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "unfollow" + }) end conn |> json("ok") end - def user_create( - conn, - %{"nickname" => nickname, "email" => email, "password" => password} - ) do - user_data = %{ - nickname: nickname, - name: nickname, - email: email, - password: password, - password_confirmation: password, - bio: "." - } + def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do + changesets = + Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> + user_data = %{ + nickname: nickname, + name: nickname, + email: email, + password: password, + password_confirmation: password, + bio: "." + } - changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) - {:ok, user} = User.register(changeset) + User.register_changeset(%User{}, user_data, need_confirmation: false) + end) + |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi -> + Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset) + end) + + case Pleroma.Repo.transaction(changesets) do + {:ok, users} -> + res = + users + |> Map.values() + |> Enum.map(fn user -> + {:ok, user} = User.post_register_action(user) + + user + end) + |> Enum.map(&AccountView.render("created.json", %{user: &1})) - conn - |> json(user.nickname) + ModerationLog.insert_log(%{ + actor: admin, + subjects: Map.values(users), + action: "create" + }) + + conn + |> json(res) + + {:error, id, changeset, _} -> + res = + Enum.map(changesets.operations, fn + {current_id, {:changeset, _current_changeset, _}} when current_id == id -> + AccountView.render("create-error.json", %{changeset: changeset}) + + {_, {:changeset, current_changeset, _}} -> + AccountView.render("create-error.json", %{changeset: current_changeset}) + end) + + conn + |> put_status(:conflict) + |> json(res) + end end def user_show(conn, %{"nickname" => nickname}) do @@ -101,23 +164,47 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def user_toggle_activation(conn, %{"nickname" => nickname}) do + def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) {:ok, updated_user} = User.deactivate(user, !user.info.deactivated) + action = if user.info.deactivated, do: "activate", else: "deactivate" + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: action + }) + conn |> json(AccountView.render("show.json", %{user: updated_user})) end - def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.tag(nicknames, tags), - do: json_response(conn, :no_content, "") + def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.tag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "tag" + }) + + json_response(conn, :no_content, "") + end end - def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.untag(nicknames, tags), - do: json_response(conn, :no_content, "") + def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.untag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "untag" + }) + + json_response(conn, :no_content, "") + end end def list_users(conn, params) do @@ -158,7 +245,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Enum.into(%{}, &{&1, true}) end - def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) + def right_add(%{assigns: %{user: admin}} = conn, %{ + "permission_group" => permission_group, + "nickname" => nickname + }) when permission_group in ["moderator", "admin"] do user = User.get_cached_by_nickname(nickname) @@ -173,6 +263,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Ecto.Changeset.change() |> Ecto.Changeset.put_embed(:info, info_cng) + ModerationLog.insert_log(%{ + action: "grant", + actor: admin, + subject: user, + permission: permission_group + }) + {:ok, _user} = User.update_and_set_cache(cng) json(conn, info) @@ -193,7 +290,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_delete( - %{assigns: %{user: %User{:nickname => admin_nickname}}} = conn, + %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname @@ -217,6 +314,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, _user} = User.update_and_set_cache(cng) + ModerationLog.insert_log(%{ + action: "revoke", + actor: admin, + subject: user, + permission: permission_group + }) + json(conn, info) end end @@ -225,15 +329,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do render_error(conn, :not_found, "No such permission_group") end - def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do + def set_activation_status(%{assigns: %{user: admin}} = conn, %{ + "nickname" => nickname, + "status" => status + }) do with {:ok, status} <- Ecto.Type.cast(:boolean, status), %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, _} <- User.deactivate(user, !status), - do: json_response(conn, :no_content, "") + {:ok, _} <- User.deactivate(user, !status) do + action = if(user.info.deactivated, do: "activate", else: "deactivate") + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: action + }) + + json_response(conn, :no_content, "") + end end - def relay_follow(conn, %{"relay_url" => target}) do + def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -243,8 +365,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def relay_unfollow(conn, %{"relay_url" => target}) do + def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -335,8 +463,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def report_update_state(conn, %{"id" => id, "state" => state}) do + def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do with {:ok, report} <- CommonAPI.update_report_state(id, state) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: report + }) + conn |> put_view(ReportView) |> render("show.json", %{report: report}) @@ -353,6 +487,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, activity} = CommonAPI.post(user, params) + ModerationLog.insert_log(%{ + action: "report_response", + actor: user, + subject: activity, + text: params["status"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -365,8 +506,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def status_update(conn, %{"id" => id} = params) do + def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do + {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"]) + + ModerationLog.insert_log(%{ + action: "status_update", + actor: admin, + subject: activity, + sensitive: sensitive, + visibility: params["visibility"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -375,10 +526,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + ModerationLog.insert_log(%{ + action: "status_delete", + actor: user, + subject_id: id + }) + json(conn, %{}) end end + def list_log(conn, params) do + {page, page_size} = page_params(params) + + log = ModerationLog.get_all(page, page_size) + + conn + |> put_view(ModerationLogView) + |> render("index.json", %{log: log}) + end + def migrate_to_db(conn, _params) do Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) json(conn, %{}) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 7e1b9c431..a96affd40 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -52,4 +52,50 @@ defmodule Pleroma.Web.AdminAPI.AccountView do invites: render_many(invites, AccountView, "invite.json", as: :invite) } end + + def render("created.json", %{user: user}) do + %{ + type: "success", + code: 200, + data: %{ + nickname: user.nickname, + email: user.email + } + } + end + + def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do + %{ + type: "error", + code: 409, + error: parse_error(errors), + data: %{ + nickname: Map.get(changes, :nickname), + email: Map.get(changes, :email) + } + } + end + + defp parse_error([]), do: "" + + defp parse_error(errors) do + ## when nickname is duplicate ap_id constraint error is raised + nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id) + email_error = Keyword.get(errors, :email) + password_error = Keyword.get(errors, :password) + + cond do + nickname_error -> + "nickname #{elem(nickname_error, 0)}" + + email_error -> + "email #{elem(email_error, 0)}" + + password_error -> + "password #{elem(password_error, 0)}" + + true -> + "" + end + end end diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex new file mode 100644 index 000000000..b3fc7cfe5 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ModerationLogView do + use Pleroma.Web, :view + + alias Pleroma.ModerationLog + + def render("index.json", %{log: log}) do + render_many(log, __MODULE__, "show.json", as: :log_entry) + end + + def render("show.json", %{log_entry: log_entry}) do + time = + log_entry.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + %{ + data: log_entry.data, + time: time, + message: ModerationLog.get_log_entry_message(log_entry) + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 72da46263..5faddc9f4 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object @@ -200,6 +201,23 @@ defmodule Pleroma.Web.CommonAPI do end end + defp check_expiry_date({:ok, nil} = res), do: res + + defp check_expiry_date({:ok, in_seconds}) do + expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds) + + if ActivityExpiration.expires_late_enough?(expiry) do + {:ok, expiry} + else + {:error, "Expiry date is too soon"} + end + end + + defp check_expiry_date(expiry_str) do + Ecto.Type.cast(:integer, expiry_str) + |> check_expiry_date() + end + def post(user, %{"status" => status} = data) do limit = Pleroma.Config.get([:instance, :limit]) @@ -226,6 +244,7 @@ defmodule Pleroma.Web.CommonAPI do context <- make_context(in_reply_to, in_reply_to_conversation), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), + {:ok, expires_at} <- check_expiry_date(data["expires_in"]), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- @@ -251,15 +270,24 @@ defmodule Pleroma.Web.CommonAPI do preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false direct? = visibility == "direct" - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => direct?} - } - |> maybe_add_list_data(user, visibility) - |> ActivityPub.create(preview?) + result = + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) + + if expires_at do + with {:ok, activity} <- result do + {:ok, _} = ActivityExpiration.create(activity, expires_at) + end + end + + result else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 61b96aba9..6958c7511 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -93,8 +93,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do Activity.t() | nil, String.t(), Participation.t() | nil - ) :: - {list(String.t()), list(String.t())} + ) :: {list(String.t()), list(String.t())} def get_to_and_cc(_, _, _, _, %Participation{} = participation) do participation = Repo.preload(participation, :recipients) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 42fbdf51b..a4ee0b5dd 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do require Pleroma.Constants alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Conversation alias Pleroma.Conversation.Participation alias Pleroma.HTML @@ -177,6 +178,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil + client_posted_this_activity = opts[:for] && user.id == opts[:for].id + + expires_at = + with true <- client_posted_this_activity, + expiration when not is_nil(expiration) <- + ActivityExpiration.get_by_activity_id(activity.id) do + expiration.scheduled_at + end + thread_muted? = case activity.thread_muted? do thread_muted? when is_boolean(thread_muted?) -> thread_muted? @@ -288,6 +298,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary_plaintext}, + expires_at: expires_at, direct_conversation_id: direct_conversation_id } } diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index fdba0f77f..07e2a4c2d 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -37,8 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do action_fallback(:errors) def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do - with {_, %User{} = user} <- - {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do + with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do RedirectController.redirector_with_meta(conn, %{user: user}) end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c2e6e8819..1ad33630c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -159,7 +159,7 @@ defmodule Pleroma.Web.Router do post("/users/unfollow", AdminAPIController, :user_unfollow) delete("/users", AdminAPIController, :user_delete) - post("/users", AdminAPIController, :user_create) + post("/users", AdminAPIController, :users_create) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) @@ -202,6 +202,8 @@ defmodule Pleroma.Web.Router do post("/config", AdminAPIController, :config_update) get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) + + get("/moderation_log", AdminAPIController, :list_log) end scope "/", Pleroma.Web.TwitterAPI do |