[#1149] Added Oban job for "activity_expiration". Merged remote-tracking branch 'remotes/upstream/develop' into 1149-oban-job-queue

# Conflicts:
#	config/config.exs
This commit is contained in:
Ivan Tashkinov 2019-08-31 14:25:43 +03:00
commit e890ea7e82
76 changed files with 2689 additions and 551 deletions

View File

@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
### Fixed ### Fixed
- Following from Osada
- Not being able to pin unlisted posts - Not being able to pin unlisted posts
- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised. - Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
- Favorites timeline doing database-intensive queries - Favorites timeline doing database-intensive queries
@ -50,8 +51,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances - Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
- MRF: fix use of unserializable keyword lists in describe() implementations - MRF: fix use of unserializable keyword lists in describe() implementations
- ActivityPub: Deactivated user deletion - ActivityPub: Deactivated user deletion
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
### Added ### Added
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour.
- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data. - Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo. - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules. Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
@ -92,6 +98,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Relays: Added a task to list relay subscriptions. - Relays: Added a task to list relay subscriptions.
- Mix Tasks: `mix pleroma.database fix_likes_collections` - Mix Tasks: `mix pleroma.database fix_likes_collections`
- Federation: Remove `likes` from objects. - Federation: Remove `likes` from objects.
- Admin API: Added moderation log
### Changed ### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
@ -193,6 +200,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Admin API: Move the user related API to `api/pleroma/admin/users` - Admin API: Move the user related API to `api/pleroma/admin/users`
- Admin API: `POST /api/pleroma/admin/users` will take list of users
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`

View File

@ -472,6 +472,7 @@
verbose: false, verbose: false,
prune: {:maxage, 60 * 60 * 24 * 7}, prune: {:maxage, 60 * 60 * 24 * 7},
queues: [ queues: [
activity_expiration: 10,
federator_incoming: 50, federator_incoming: 50,
federator_outgoing: 50, federator_outgoing: 50,
web_push: 50, web_push: 50,
@ -578,16 +579,9 @@
config :http_signatures, config :http_signatures,
adapter: Pleroma.Signature adapter: Pleroma.Signature
config :pleroma, :rate_limit, config :pleroma, :rate_limit, nil
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25}, config :pleroma, Pleroma.ActivityExpiration, enabled: true
relations_actions: {10_000, 10},
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},
status_id_action: {60_000, 3},
password_reset: {1_800_000, 5},
account_confirmation_resend: {8_640_000, 5},
ap_routes: {60_000, 15}
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.

View File

@ -88,11 +88,10 @@
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
try do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
rescue else
_ -> IO.puts(
IO.puts( "You may want to create test.secret.exs to declare custom database connection parameters."
"You may want to create test.secret.exs to declare custom database connection parameters." )
)
end end

View File

@ -694,3 +694,27 @@ Compile time settings (need instance reboot):
] ]
} }
``` ```
## `/api/pleroma/admin/moderation_log`
### Get moderation log
- Method `GET`
- Params:
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`)
- Response:
```json
[
{
"data": {
"actor": {
"id": 1,
"nickname": "lain"
},
"action": "relay_follow"
},
"time": 1502812026, // timestamp
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
}
]
```

View File

@ -25,6 +25,7 @@ Has these additional fields under the `pleroma` object:
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
## Attachments ## Attachments
@ -86,6 +87,7 @@ Additional parameters can be added to the JSON body/Form data:
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.
- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. - `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
## PATCH `/api/v1/update_credentials` ## PATCH `/api/v1/update_credentials`

View File

@ -8,7 +8,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
* `filters`: List of `Pleroma.Upload.Filter` to use. * `filters`: List of `Pleroma.Upload.Filter` to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` * `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. * `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
@ -497,6 +497,10 @@ config :auto_linker,
* `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `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 * `enabled`: whether scheduled activities are sent to the job queue to be executed
## Pleroma.ActivityExpiration
# `enabled`: whether expired activities will be sent to the job queue to be deleted
## Pleroma.Web.Auth.Authenticator ## Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator * `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
@ -669,6 +673,8 @@ This will probably take a long time.
## :rate_limit ## :rate_limit
This is an advanced feature and disabled by default.
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds. * The first element: `scale` (Integer). The time scale in milliseconds.

View File

@ -71,26 +71,26 @@ server {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
# and `localhost.` resolves to [::0] on some systems: see issue #930 # and `localhost.` resolves to [::0] on some systems: see issue #930
proxy_pass http://127.0.0.1:4000; proxy_pass http://127.0.0.1:4000;
client_max_body_size 16m; client_max_body_size 16m;
} }
location ~ ^/(media|proxy) { location ~ ^/(media|proxy) {
proxy_cache pleroma_media_cache; proxy_cache pleroma_media_cache;
slice 1m; slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range; proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range; proxy_set_header Range $slice_range;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_cache_valid 200 206 301 304 1h; proxy_cache_valid 200 206 301 304 1h;
proxy_cache_lock on; proxy_cache_lock on;
proxy_ignore_client_abort on; proxy_ignore_client_abort on;
proxy_buffering on; proxy_buffering on;
chunked_transfer_encoding on; chunked_transfer_encoding on;
proxy_ignore_headers Cache-Control; proxy_ignore_headers Cache-Control;
proxy_hide_header Cache-Control; proxy_hide_header Cache-Control;
proxy_pass http://localhost:4000; proxy_pass http://127.0.0.1:4000;
} }
} }

View File

@ -6,6 +6,7 @@ defmodule Pleroma.Activity do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Bookmark alias Pleroma.Bookmark
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -59,6 +60,8 @@ defmodule Pleroma.Activity do
# typical case. # typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id) has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
timestamps() timestamps()
end end

View File

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Queries do
@moduledoc """
Contains queries for Activity.
"""
import Ecto.Query, only: [from: 2]
@type query :: Ecto.Queryable.t() | Activity.t()
alias Pleroma.Activity
@spec by_actor(query, String.t()) :: query
def by_actor(query \\ Activity, actor) do
from(
activity in query,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor)
)
end
@spec by_object_id(query, String.t()) :: query
def by_object_id(query \\ Activity, object_id) do
from(activity in query,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^object_id
)
)
end
@spec by_type(query, String.t()) :: query
def by_type(query \\ Activity, activity_type) do
from(
activity in query,
where: fragment("(?)->>'type' = ?", activity.data, ^activity_type)
)
end
@spec limit(query, pos_integer()) :: query
def limit(query \\ Activity, limit) do
from(activity in query, limit: ^limit)
end
end

View File

@ -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

View File

@ -0,0 +1,71 @@
# 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
alias Pleroma.Workers.BackgroundWorker
require Logger
use GenServer
import Ecto.Query
defdelegate worker_args(queue), to: Pleroma.Workers.Helper
@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 ->
%{
"op" => "activity_expiration",
"activity_expiration_id" => expiration.id
}
|> BackgroundWorker.new(worker_args(:activity_expiration))
|> Repo.insert()
end)
schedule_next()
{:noreply, state}
end
defp schedule_next do
Process.send_after(self(), :perform, @schedule_interval)
end
end

View File

@ -36,7 +36,8 @@ def start(_type, _args) do
Pleroma.Emoji, Pleroma.Emoji,
Pleroma.Captcha, Pleroma.Captcha,
Pleroma.FlakeId, Pleroma.FlakeId,
Pleroma.ScheduledActivityWorker Pleroma.ScheduledActivityWorker,
Pleroma.ActivityExpirationWorker
] ++ ] ++
cachex_children() ++ cachex_children() ++
hackney_pool_children() ++ hackney_pool_children() ++

View File

@ -109,15 +109,19 @@ def rename(%Pleroma.List{} = list, title) do
end end
def create(title, %User{} = creator) do def create(title, %User{} = creator) do
list = %Pleroma.List{user_id: creator.id, title: title} changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title})
Repo.transaction(fn -> if changeset.valid? do
list = Repo.insert!(list) Repo.transaction(fn ->
list = Repo.insert!(changeset)
list list
|> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}")
|> Repo.update!() |> Repo.update!()
end) end)
else
{:error, changeset}
end
end end
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do def follow(%Pleroma.List{following: following} = list, %User{} = followed) do

View File

@ -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

View File

@ -150,8 +150,6 @@ def set_cache(%Object{data: %{"id" => ap_id}} = object) do
def update_and_set_cache(changeset) do def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do with {:ok, object} <- Repo.update(changeset) do
set_cache(object) set_cache(object)
else
e -> e
end end
end end

View File

@ -117,9 +117,7 @@ defp maybe_date_fetch(headers, date) do
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.info("Fetching object #{id} via AP") Logger.info("Fetching object #{id} via AP")
date = date = Pleroma.Signature.signed_date()
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
headers = headers =
[{:Accept, "application/activity+json"}] [{:Accept, "application/activity+json"}]

View File

@ -53,4 +53,10 @@ def sign(%User{} = user, headers) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end end
end end
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
end end

View File

@ -333,7 +333,13 @@ defp autofollow_users(user) do
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset), 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} <- set_cache(user),
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do {:ok, _} <- try_send_confirmation_email(user) do

View File

@ -49,7 +49,7 @@ defmodule Pleroma.User.Info do
field(:mascot, :map, default: nil) field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: []) field(:emoji, {:array, :map}, default: [])
field(:pleroma_settings_store, :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(:raw_fields, {:array, :map}, default: [])
field(:notification_settings, :map, field(:notification_settings, :map,
@ -422,7 +422,7 @@ def remove_reblog_mute(info, ap_id) do
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …] # 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) limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
attachment attachment
@ -431,6 +431,8 @@ def fields(%{fields: [], source_data: %{"attachment" => attachment}}) do
|> Enum.take(limit) |> Enum.take(limit)
end end
def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields def fields(%{fields: fields}), do: fields
def follow_information_update(info, params) do def follow_information_update(info, params) do

View File

@ -142,7 +142,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
# Splice in the child object if we have one. # Splice in the child object if we have one.
activity = activity =
if !is_nil(object) do if not is_nil(object) do
Map.put(activity, :object, object) Map.put(activity, :object, object)
else else
activity activity
@ -336,12 +336,7 @@ def like(
end end
end end
def unlike( def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id), unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local), {:ok, unlike_activity} <- insert(unlike_data, local),

View File

@ -41,7 +41,7 @@ def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
@ -53,7 +53,7 @@ def object(conn, %{"uuid" => uuid}) do
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do {_, true} <- {:public?, Visibility.is_public?(object)} do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: object})) |> json(ObjectView.render("object.json", %{object: object}))
else else
{:public?, false} -> {:public?, false} ->
@ -69,7 +69,7 @@ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page)) |> json(ObjectView.render("likes.json", ap_id, likes, page))
else else
{:public?, false} -> {:public?, false} ->
@ -83,7 +83,7 @@ def object_likes(conn, %{"uuid" => uuid}) do
{_, true} <- {:public?, Visibility.is_public?(object)}, {_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do likes <- Utils.get_object_likes(object) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes)) |> json(ObjectView.render("likes.json", ap_id, likes))
else else
{:public?, false} -> {:public?, false} ->
@ -96,7 +96,7 @@ def activity(conn, %{"uuid" => uuid}) do
%Activity{} = activity <- Activity.normalize(ap_id), %Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity})) |> json(ObjectView.render("object.json", %{object: activity}))
else else
{:public?, false} -> {:public?, false} ->
@ -104,6 +104,13 @@ def activity(conn, %{"uuid" => uuid}) do
end end
end end
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()}))
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
@ -112,12 +119,12 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) |> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
else else
{:show_follows, _} -> {:show_follows, _} ->
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> send_resp(403, "") |> send_resp(403, "")
end end
end end
@ -126,11 +133,18 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user})) |> json(UserView.render("following.json", %{user: user, for: for_user}))
end end
end end
# GET /relay/followers
def followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
@ -139,12 +153,12 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
else else
{:show_followers, _} -> {:show_followers, _} ->
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> send_resp(403, "") |> send_resp(403, "")
end end
end end
@ -153,7 +167,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user})) |> json(UserView.render("followers.json", %{user: user, for: for_user}))
end end
end end
@ -162,7 +176,7 @@ def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
end end
end end
@ -210,7 +224,7 @@ def inbox(conn, params) do
defp represent_service_actor(%User{} = user, conn) do defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do with {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
@ -231,7 +245,7 @@ def internal_fetch(conn, _params) do
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
end end
@ -240,7 +254,7 @@ def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do if nickname == user.nickname do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else else
err = err =
@ -295,42 +309,42 @@ def handle_user_activity(_, _) do
end end
def update_outbox( def update_outbox(
%{assigns: %{user: user}} = conn, %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params %{"nickname" => nickname} = params
) do ) do
if nickname == user.nickname do actor = user.ap_id()
actor = user.ap_id()
params = params =
params params
|> Map.drop(["id"]) |> Map.drop(["id"])
|> Map.put("actor", actor) |> Map.put("actor", actor)
|> Transmogrifier.fix_addressing() |> Transmogrifier.fix_addressing()
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn
|> put_status(:created)
|> put_resp_header("location", activity.data["id"])
|> json(activity.data)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
end
else
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn conn
|> put_status(:forbidden) |> put_status(:created)
|> json(err) |> put_resp_header("location", activity.data["id"])
|> json(activity.data)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
end end
end end
def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)

View File

@ -25,11 +25,15 @@ defp score_displayname("fedibot"), do: 1.0
defp score_displayname(_), do: 0.0 defp score_displayname(_), do: 0.0
defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
# nickname will always be a binary string because it's generated by Pleroma. # nickname will be a binary string except when following a relay
nick_score = nick_score =
nickname if is_binary(nickname) do
|> String.downcase() nickname
|> score_nickname() |> String.downcase()
|> score_nickname()
else
0.0
end
# displayname will either be a binary string or nil, if a displayname isn't set. # displayname will either be a binary string or nil, if a displayname isn't set.
name_score = name_score =

View File

@ -50,9 +50,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = date = Pleroma.Signature.signed_date()
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature = signature =
Pleroma.Signature.sign(actor, %{ Pleroma.Signature.sign(actor, %{

View File

@ -22,13 +22,7 @@ def follow(target_instance) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else
{:error, _} = error -> error -> format_error(error)
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end end
end end
@ -37,16 +31,11 @@ def unfollow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
User.unfollow(local_user, target_user)
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else
{:error, _} = error -> error -> format_error(error)
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end end
end end
@ -56,11 +45,16 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
%Object{} = object <- Object.normalize(activity) do %Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false) ActivityPub.announce(user, object, nil, true, false)
else else
e -> error -> format_error(error)
Logger.error("error: #{inspect(e)}")
{:error, inspect(e)}
end end
end end
def publish(_), do: {:error, "Not implemented"} def publish(_), do: {:error, "Not implemented"}
defp format_error({:error, error}), do: format_error(error)
defp format_error(error) do
Logger.error("error: #{inspect(error)}")
{:error, error}
end
end end

View File

@ -467,8 +467,10 @@ def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options _options
) do ) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <-
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},

View File

@ -166,6 +166,7 @@ def create_context(context) do
@doc """ @doc """
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
if Pleroma.Config.get!([:instance, :federating]) do if Pleroma.Config.get!([:instance, :federating]) do
Pleroma.Web.Federator.publish(activity) Pleroma.Web.Federator.publish(activity)
@ -249,46 +250,27 @@ def insert_full_object(map), do: {:ok, map, nil}
@doc """ @doc """
Returns an existing like if a user already liked an object Returns an existing like if a user already liked an object
""" """
@spec get_existing_like(String.t(), map()) :: Activity.t() | nil
def get_existing_like(actor, %{data: %{"id" => id}}) do def get_existing_like(actor, %{data: %{"id" => id}}) do
query = actor
from( |> Activity.Queries.by_actor()
activity in Activity, |> Activity.Queries.by_object_id(id)
where: fragment("(?)->>'actor' = ?", activity.data, ^actor), |> Activity.Queries.by_type("Like")
# this is to use the index |> Activity.Queries.limit(1)
where: |> Repo.one()
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query)
end end
@doc """ @doc """
Returns like activities targeting an object Returns like activities targeting an object
""" """
def get_object_likes(%{data: %{"id" => id}}) do def get_object_likes(%{data: %{"id" => id}}) do
query = id
from( |> Activity.Queries.by_object_id()
activity in Activity, |> Activity.Queries.by_type("Like")
# this is to use the index |> Repo.all()
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.all(query)
end end
@spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data( def make_like_data(
%User{ap_id: ap_id} = actor, %User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object, %{data: %{"actor" => object_actor_id, "id" => id}} = object,
@ -308,7 +290,7 @@ def make_like_data(
|> List.delete(actor.ap_id) |> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address) |> List.delete(object_actor.follower_address)
data = %{ %{
"type" => "Like", "type" => "Like",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -316,38 +298,49 @@ def make_like_data(
"cc" => cc, "cc" => cc,
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
@spec update_element_in_object(String.t(), list(any), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do def update_element_in_object(property, element, object) do
with new_data <- data =
object.data Map.merge(
|> Map.put("#{property}_count", length(element)) object.data,
|> Map.put("#{property}s", element), %{"#{property}_count" => length(element), "#{property}s" => element}
changeset <- Changeset.change(object, data: new_data), )
{:ok, object} <- Object.update_and_set_cache(changeset) do
{:ok, object} object
end |> Changeset.change(data: data)
|> Object.update_and_set_cache()
end end
def update_likes_in_object(likes, object) do @spec add_like_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
[actor | fetch_likes(object)]
|> Enum.uniq()
|> update_likes_in_object(object)
end
@spec remove_like_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
object
|> fetch_likes()
|> List.delete(actor)
|> update_likes_in_object(object)
end
defp update_likes_in_object(likes, object) do
update_element_in_object("like", likes, object) update_element_in_object("like", likes, object)
end end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do defp fetch_likes(object) do
likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] if is_list(object.data["likes"]) do
object.data["likes"]
with likes <- [actor | likes] |> Enum.uniq() do else
update_likes_in_object(likes, object) []
end
end
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
with likes <- likes |> List.delete(actor) do
update_likes_in_object(likes, object)
end end
end end
@ -398,7 +391,7 @@ def make_follow_data(
%User{ap_id: followed_id} = _followed, %User{ap_id: followed_id} = _followed,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Follow", "type" => "Follow",
"actor" => follower_id, "actor" => follower_id,
"to" => [followed_id], "to" => [followed_id],
@ -406,10 +399,7 @@ def make_follow_data(
"object" => followed_id, "object" => followed_id,
"state" => "pending" "state" => "pending"
} }
|> maybe_put("id", activity_id)
data = if activity_id, do: Map.put(data, "id", activity_id), else: data
data
end end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@ -471,7 +461,7 @@ def make_announce_data(
activity_id, activity_id,
false false
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -479,8 +469,7 @@ def make_announce_data(
"cc" => [], "cc" => [],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_announce_data( def make_announce_data(
@ -489,7 +478,7 @@ def make_announce_data(
activity_id, activity_id,
true true
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -497,8 +486,7 @@ def make_announce_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
@doc """ @doc """
@ -509,7 +497,7 @@ def make_unannounce_data(
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context}} = activity,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
@ -517,8 +505,7 @@ def make_unannounce_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_unlike_data( def make_unlike_data(
@ -526,7 +513,7 @@ def make_unlike_data(
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context}} = activity,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
@ -534,8 +521,7 @@ def make_unlike_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def add_announce_to_object( def add_announce_to_object(
@ -566,14 +552,13 @@ def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
#### Unfollow-related helpers #### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do def make_unfollow_data(follower, followed, follow_activity, activity_id) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => follower.ap_id, "actor" => follower.ap_id,
"to" => [followed.ap_id], "to" => [followed.ap_id],
"object" => follow_activity.data "object" => follow_activity.data
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
#### Block-related helpers #### Block-related helpers
@ -603,25 +588,23 @@ def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
end end
def make_block_data(blocker, blocked, activity_id) do def make_block_data(blocker, blocked, activity_id) do
data = %{ %{
"type" => "Block", "type" => "Block",
"actor" => blocker.ap_id, "actor" => blocker.ap_id,
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => blocked.ap_id "object" => blocked.ap_id
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do def make_unblock_data(blocker, blocked, block_activity, activity_id) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => blocker.ap_id, "actor" => blocker.ap_id,
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => block_activity.data "object" => block_activity.data
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
#### Create-related helpers #### Create-related helpers
@ -792,4 +775,7 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
Repo.all(query) Repo.all(query)
end end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -12,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -25,52 +27,113 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors) action_fallback(:errors)
def user_delete(conn, %{"nickname" => nickname}) do def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
|> User.delete() User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
subject: user,
action: "delete"
})
conn conn
|> json(nickname) |> json(nickname)
end 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), with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed) User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end end
conn conn
|> json("ok") |> json("ok")
end 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), with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed) User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end end
conn conn
|> json("ok") |> json("ok")
end end
def user_create( def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
conn, changesets =
%{"nickname" => nickname, "email" => email, "password" => password} Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
) do user_data = %{
user_data = %{ nickname: nickname,
nickname: nickname, name: nickname,
name: nickname, email: email,
email: email, password: password,
password: password, password_confirmation: password,
password_confirmation: password, bio: "."
bio: "." }
}
changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) User.register_changeset(%User{}, user_data, need_confirmation: false)
{:ok, user} = User.register(changeset) end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
conn case Pleroma.Repo.transaction(changesets) do
|> json(user.nickname) {: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}))
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 end
def user_show(conn, %{"nickname" => nickname}) do def user_show(conn, %{"nickname" => nickname}) do
@ -101,23 +164,47 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
end end
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) user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated) {: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 conn
|> json(AccountView.render("show.json", %{user: updated_user})) |> json(AccountView.render("show.json", %{user: updated_user}))
end end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags), with {:ok, _} <- User.tag(nicknames, tags) do
do: json_response(conn, :no_content, "") ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "tag"
})
json_response(conn, :no_content, "")
end
end end
def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.untag(nicknames, tags), with {:ok, _} <- User.untag(nicknames, tags) do
do: json_response(conn, :no_content, "") ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "untag"
})
json_response(conn, :no_content, "")
end
end end
def list_users(conn, params) do def list_users(conn, params) do
@ -158,7 +245,10 @@ defp maybe_parse_filters(filters) do
|> Enum.into(%{}, &{&1, true}) |> Enum.into(%{}, &{&1, true})
end 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 when permission_group in ["moderator", "admin"] do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
@ -173,6 +263,13 @@ def right_add(conn, %{"permission_group" => permission_group, "nickname" => nick
|> Ecto.Changeset.change() |> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_cng) |> 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) {:ok, _user} = User.update_and_set_cache(cng)
json(conn, info) json(conn, info)
@ -193,7 +290,7 @@ def right_get(conn, %{"nickname" => nickname}) do
end end
def right_delete( def right_delete(
%{assigns: %{user: %User{:nickname => admin_nickname}}} = conn, %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
%{ %{
"permission_group" => permission_group, "permission_group" => permission_group,
"nickname" => nickname "nickname" => nickname
@ -217,6 +314,13 @@ def right_delete(
{:ok, _user} = User.update_and_set_cache(cng) {:ok, _user} = User.update_and_set_cache(cng)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: user,
permission: permission_group
})
json(conn, info) json(conn, info)
end end
end end
@ -225,15 +329,33 @@ def right_delete(conn, _) do
render_error(conn, :not_found, "No such permission_group") render_error(conn, :not_found, "No such permission_group")
end 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), with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_cached_by_nickname(nickname), %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status), {:ok, _} <- User.deactivate(user, !status) do
do: json_response(conn, :no_content, "") 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 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 with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{
action: "relay_follow",
actor: admin,
target: target
})
json(conn, target) json(conn, target)
else else
_ -> _ ->
@ -243,8 +365,14 @@ def relay_follow(conn, %{"relay_url" => target}) do
end end
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 with {:ok, _message} <- Relay.unfollow(target) do
ModerationLog.insert_log(%{
action: "relay_unfollow",
actor: admin,
target: target
})
json(conn, target) json(conn, target)
else else
_ -> _ ->
@ -335,8 +463,14 @@ def report_show(conn, %{"id" => id}) do
end end
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 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: report
})
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("show.json", %{report: report}) |> render("show.json", %{report: report})
@ -353,6 +487,13 @@ def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
{:ok, activity} = CommonAPI.post(user, params) {:ok, activity} = CommonAPI.post(user, params)
ModerationLog.insert_log(%{
action: "report_response",
actor: user,
subject: activity,
text: params["status"]
})
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> render("status.json", %{activity: activity}) |> render("status.json", %{activity: activity})
@ -365,8 +506,18 @@ def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
end end
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 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 conn
|> put_view(StatusView) |> put_view(StatusView)
|> render("status.json", %{activity: activity}) |> render("status.json", %{activity: activity})
@ -375,10 +526,26 @@ def status_update(conn, %{"id" => id} = params) do
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
ModerationLog.insert_log(%{
action: "status_delete",
actor: user,
subject_id: id
})
json(conn, %{}) json(conn, %{})
end end
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 def migrate_to_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
json(conn, %{}) json(conn, %{})

View File

@ -52,4 +52,50 @@ def render("invites.json", %{invites: invites}) do
invites: render_many(invites, AccountView, "invite.json", as: :invite) invites: render_many(invites, AccountView, "invite.json", as: :invite)
} }
end 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 end

View File

@ -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

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
@ -200,6 +201,23 @@ def get_replied_to_visibility(activity) do
end end
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 def post(user, %{"status" => status} = data) do
limit = Pleroma.Config.get([:instance, :limit]) limit = Pleroma.Config.get([:instance, :limit])
@ -226,6 +244,7 @@ def post(user, %{"status" => status} = data) do
context <- make_context(in_reply_to, in_reply_to_conversation), context <- make_context(in_reply_to, in_reply_to_conversation),
cw <- data["spoiler_text"] || "", cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
{:ok, expires_at} <- check_expiry_date(data["expires_in"]),
full_payload <- String.trim(status <> cw), full_payload <- String.trim(status <> cw),
:ok <- validate_character_limit(full_payload, attachments, limit), :ok <- validate_character_limit(full_payload, attachments, limit),
object <- object <-
@ -251,15 +270,24 @@ def post(user, %{"status" => status} = data) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct" direct? = visibility == "direct"
%{ result =
to: to, %{
actor: user, to: to,
context: context, actor: user,
object: object, context: context,
additional: %{"cc" => cc, "directMessage" => direct?} object: object,
} additional: %{"cc" => cc, "directMessage" => direct?}
|> maybe_add_list_data(user, visibility) }
|> ActivityPub.create(preview?) |> 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 else
{:private_to_public, true} -> {:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")} {:error, dgettext("errors", "The message visibility must be direct")}

View File

@ -93,8 +93,7 @@ def attachments_from_ids_descs(ids, descs_str) do
Activity.t() | nil, Activity.t() | nil,
String.t(), String.t(),
Participation.t() | nil Participation.t() | nil
) :: ) :: {list(String.t()), list(String.t())}
{list(String.t()), list(String.t())}
def get_to_and_cc(_, _, _, _, %Participation{} = participation) do def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
participation = Repo.preload(participation, :recipients) participation = Repo.preload(participation, :recipients)

View File

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FallbackController do
use Pleroma.Web, :controller
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
error_message =
changeset
|> Ecto.Changeset.traverse_errors(fn {message, _opt} -> message end)
|> Enum.map_join(", ", fn {_k, v} -> v end)
conn
|> put_status(:unprocessable_entity)
|> json(%{error: error_message})
end
def call(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Record not found")
end
def call(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
|> json(%{error: error_message})
end
def call(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
end

View File

@ -0,0 +1,84 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
plug(:list_by_id_and_user when action not in [:index, :create])
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# GET /api/v1/lists
def index(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
render(conn, "index.json", lists: lists)
end
# POST /api/v1/lists
def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
end
# GET /api/v1/lists/:id
def show(%{assigns: %{list: list}} = conn, _) do
render(conn, "show.json", list: list)
end
# PUT /api/v1/lists/:id
def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
end
# DELETE /api/v1/lists/:id
def delete(%{assigns: %{list: list}} = conn, _) do
with {:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
end
end
# GET /api/v1/lists/:id/accounts
def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do
with {:ok, users} <- Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
|> render("accounts.json", for: user, users: users, as: :user)
end
end
# POST /api/v1/lists/:id/accounts
def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
# DELETE /api/v1/lists/:id/accounts
def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
end
end
end

View File

@ -83,7 +83,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"
action_fallback(:errors) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def create_app(conn, params) do def create_app(conn, params) do
scopes = Scopes.fetch_scopes(params, ["read"]) scopes = Scopes.fetch_scopes(params, ["read"])
@ -189,7 +189,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
info_cng = User.Info.profile_update(user.info, info_params) info_cng = User.Info.profile_update(user.info, info_params)
with changeset <- User.update_changeset(user, user_params), with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), changeset <- Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do if original_user != user do
CommonAPI.update(user) CommonAPI.update(user)
@ -225,7 +225,7 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
with new_info <- %{"banner" => %{}}, with new_info <- %{"banner" => %{}},
info_cng <- User.Info.profile_update(user.info, new_info), info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)
@ -237,7 +237,7 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data}, new_info <- %{"banner" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info), info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
@ -249,7 +249,7 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
with new_info <- %{"background" => %{}}, with new_info <- %{"background" => %{}},
info_cng <- User.Info.profile_update(user.info, new_info), info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do {:ok, _user} <- User.update_and_set_cache(changeset) do
json(conn, %{url: nil}) json(conn, %{url: nil})
end end
@ -259,7 +259,7 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background), with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data}, new_info <- %{"background" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info), info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do {:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
@ -806,8 +806,8 @@ def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
user_changeset = user_changeset =
user user
|> Ecto.Changeset.change() |> Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_changeset) |> Changeset.put_embed(:info, info_changeset)
{:ok, _user} = User.update_and_set_cache(user_changeset) {:ok, _user} = User.update_and_set_cache(user_changeset)
@ -1205,88 +1205,12 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
end end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e -> render_error(conn, :not_found, "Record not found")
end
end
def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
lists = Pleroma.List.get_lists_account_belongs(user, account_id) lists = Pleroma.List.get_lists_account_belongs(user, account_id)
res = ListView.render("lists.json", lists: lists) res = ListView.render("lists.json", lists: lists)
json(conn, res) json(conn, res)
end end
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
else
_e ->
json(conn, dgettext("errors", "error"))
end
end
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
end
end
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
|> render("accounts.json", %{for: user, users: users, as: :user})
end
end
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, list} <- Pleroma.List.rename(list, title) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e ->
json(conn, dgettext("errors", "error"))
end
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params = params =
@ -1420,8 +1344,8 @@ def index(%{assigns: %{user: user}} = conn, _params) do
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
info_cng = User.Info.mastodon_settings_update(user.info, settings) info_cng = User.Info.mastodon_settings_update(user.info, settings)
with changeset <- Ecto.Changeset.change(user), with changeset <- Changeset.change(user),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), changeset <- Changeset.put_embed(changeset, :info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do {:ok, _user} <- User.update_and_set_cache(changeset) do
json(conn, %{}) json(conn, %{})
else else
@ -1485,7 +1409,7 @@ defp get_or_make_app do
{:ok, app} {:ok, app}
else else
app app
|> Ecto.Changeset.change(%{scopes: scopes}) |> Changeset.change(%{scopes: scopes})
|> Repo.update() |> Repo.update()
end end
@ -1587,35 +1511,6 @@ def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
json(conn, %{}) json(conn, %{})
end end
# 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(:unprocessable_entity)
|> json(%{error: error_message})
end
def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Record not found")
end
def errors(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
|> json(%{error: error_message})
end
def errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
def suggestions(%{assigns: %{user: user}} = conn, _) do def suggestions(%{assigns: %{user: user}} = conn, _) do
suggestions = Config.get(:suggestions) suggestions = Config.get(:suggestions)

View File

@ -64,8 +64,6 @@ def errors(conn, {:error, :not_found}) do
end end
def errors(conn, _) do def errors(conn, _) do
conn Pleroma.Web.MastodonAPI.FallbackController.call(conn, nil)
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end end
end end

View File

@ -6,11 +6,11 @@ defmodule Pleroma.Web.MastodonAPI.ListView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.ListView
def render("lists.json", %{lists: lists} = opts) do def render("index.json", %{lists: lists} = opts) do
render_many(lists, ListView, "list.json", opts) render_many(lists, ListView, "show.json", opts)
end end
def render("list.json", %{list: list}) do def render("show.json", %{list: list}) do
%{ %{
id: to_string(list.id), id: to_string(list.id),
title: list.title title: list.title

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
require Pleroma.Constants require Pleroma.Constants
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.HTML alias Pleroma.HTML
@ -177,6 +178,15 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil 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? = thread_muted? =
case activity.thread_muted? do case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted? thread_muted? when is_boolean(thread_muted?) -> thread_muted?
@ -288,6 +298,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext}, content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext}, spoiler_text: %{"text/plain" => summary_plaintext},
expires_at: expires_at,
direct_conversation_id: direct_conversation_id direct_conversation_id: direct_conversation_id
} }
} }

View File

@ -37,8 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
action_fallback(:errors) action_fallback(:errors)
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
{:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
RedirectController.redirector_with_meta(conn, %{user: user}) RedirectController.redirector_with_meta(conn, %{user: user})
end end
end end

View File

@ -133,6 +133,10 @@ defmodule Pleroma.Web.Router do
}) })
end end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api) pipe_through(:pleroma_api)
@ -155,7 +159,7 @@ defmodule Pleroma.Web.Router do
post("/users/unfollow", AdminAPIController, :user_unfollow) post("/users/unfollow", AdminAPIController, :user_unfollow)
delete("/users", AdminAPIController, :user_delete) delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :user_create) post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
put("/users/tag", AdminAPIController, :tag_users) put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users) delete("/users/tag", AdminAPIController, :untag_users)
@ -198,6 +202,8 @@ defmodule Pleroma.Web.Router do
post("/config", AdminAPIController, :config_update) post("/config", AdminAPIController, :config_update)
get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/moderation_log", AdminAPIController, :list_log)
end end
scope "/", Pleroma.Web.TwitterAPI do scope "/", Pleroma.Web.TwitterAPI do
@ -306,9 +312,9 @@ defmodule Pleroma.Web.Router do
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
get("/lists", MastodonAPIController, :get_lists) get("/lists", ListController, :index)
get("/lists/:id", MastodonAPIController, :get_list) get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts) get("/lists/:id/accounts", ListController, :list_accounts)
get("/domain_blocks", MastodonAPIController, :domain_blocks) get("/domain_blocks", MastodonAPIController, :domain_blocks)
@ -349,12 +355,12 @@ defmodule Pleroma.Web.Router do
post("/media", MastodonAPIController, :upload) post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media) put("/media/:id", MastodonAPIController, :update_media)
delete("/lists/:id", MastodonAPIController, :delete_list) delete("/lists/:id", ListController, :delete)
post("/lists", MastodonAPIController, :create_list) post("/lists", ListController, :create)
put("/lists/:id", MastodonAPIController, :rename_list) put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list) post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list) delete("/lists/:id/accounts", ListController, :remove_from_list)
post("/filters", MastodonAPIController, :create_filter) post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter) get("/filters/:id", MastodonAPIController, :get_filter)
@ -686,7 +692,14 @@ defmodule Pleroma.Web.Router do
pipe_through(:ap_service_actor) pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay) get("/", ActivityPubController, :relay)
post("/inbox", ActivityPubController, :inbox)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :following, assigns: %{relay: true})
get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
end end
scope "/internal/fetch", Pleroma.Web.ActivityPub do scope "/internal/fetch", Pleroma.Web.ActivityPub do

View File

@ -69,4 +69,11 @@ def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id},
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity) Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
end end
def perform(
%{"op" => "activity_expiration", "activity_expiration_id" => activity_expiration_id},
_job
) do
Pleroma.ActivityExpirationWorker.perform(:execute, activity_expiration_id)
end
end end

View File

@ -0,0 +1,10 @@
defmodule Pleroma.Repo.Migrations.AddExpirationsTable do
use Ecto.Migration
def change do
create_if_not_exists table(:activity_expirations) do
add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all))
add(:scheduled_at, :naive_datetime, null: false)
end
end
end

View File

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.CreateModerationLog do
use Ecto.Migration
def change do
create table(:moderation_log) do
add(:data, :map)
timestamps()
end
end
end

View File

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityExpirationTest do
use Pleroma.DataCase
alias Pleroma.ActivityExpiration
import Pleroma.Factory
test "finds activities due to be deleted only" do
activity = insert(:note_activity)
expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id})
activity2 = insert(:note_activity)
insert(:expiration_in_the_future, %{activity_id: activity2.id})
expirations = ActivityExpiration.due_expirations()
assert length(expirations) == 1
assert hd(expirations) == expiration_due
end
test "denies expirations that don't live long enough" do
activity = insert(:note_activity)
now = NaiveDateTime.utc_now()
assert {:error, _} = ActivityExpiration.create(activity, now)
end
end

View File

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityExpirationWorkerTest do
use Pleroma.DataCase
alias Pleroma.Activity
import Pleroma.Factory
test "deletes an activity" do
activity = insert(:note_activity)
expiration = insert(:expiration_in_the_past, %{activity_id: activity.id})
Pleroma.ActivityExpirationWorker.perform(:execute, expiration.id)
refute Repo.get(Activity, activity.id)
end
end

View File

@ -166,4 +166,13 @@ test "find all statuses for unauthenticated users when `limit_to_local_content`
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end end
end end
test "add an activity with an expiration" do
activity = insert(:note_activity)
insert(:expiration_in_the_future, %{activity_id: activity.id})
Pleroma.ActivityExpiration
|> where([a], a.activity_id == ^activity.id)
|> Repo.one!()
end
end end

View File

@ -0,0 +1,56 @@
{
"@context":[
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://apfed.club/apschema/v1.4"
],
"id":"https://apfed.club/follow/9",
"type":"Follow",
"actor":{
"type":"Person",
"id":"https://apfed.club/channel/indio",
"preferredUsername":"indio",
"name":"Indio",
"updated":"2019-08-20T23:52:34Z",
"icon":{
"type":"Image",
"mediaType":"image/jpeg",
"updated":"2019-08-20T23:53:37Z",
"url":"https://apfed.club/photo/profile/l/2",
"height":300,
"width":300
},
"url":"https://apfed.club/channel/indio",
"inbox":"https://apfed.club/inbox/indio",
"outbox":"https://apfed.club/outbox/indio",
"followers":"https://apfed.club/followers/indio",
"following":"https://apfed.club/following/indio",
"endpoints":{
"sharedInbox":"https://apfed.club/inbox"
},
"publicKey":{
"id":"https://apfed.club/channel/indio",
"owner":"https://apfed.club/channel/indio",
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6
\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR
\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS
\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE
\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"
}
},
"object":"https://pleroma.site/users/kaniini",
"to":[
"https://pleroma.site/users/kaniini"
],
"signature":{
"@context":[
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type":"RsaSignature2017",
"nonce":"52c035e0a9e81dce8b486159204e97c22637e91f75cdfad5378de91de68e9117",
"creator":"https://apfed.club/channel/indio/public_key_pem",
"created":"2019-08-22T03:38:02Z",
"signatureValue":"oVliRCIqNIh6yUp851dYrF0y21aHp3Rz6VkIpW1pFMWfXuzExyWSfcELpyLseeRmsw5bUu9zJkH44B4G2LiJQKA9UoEQDjrDMZBmbeUpiQqq3DVUzkrBOI8bHZ7xyJ/CjSZcNHHh0MHhSKxswyxWMGi4zIqzkAZG3vRRgoPVHdjPm00sR3B8jBLw1cjoffv+KKeM/zEUpe13gqX9qHAWHHqZepxgSWmq+EKOkRvHUPBXiEJZfXzc5uW+vZ09F3WBYmaRoy8Y0e1P29fnRLqSy7EEINdrHaGclRqoUZyiawpkgy3lWWlynesV/HiLBR7EXT79eKstxf4wfTDaPKBCfTCsOWuMWHr7Genu37ew2/t7eiBGqCwwW12ylhml/OLHgNK3LOhmRABhtfpaFZSxfDVnlXfaLpY1xekVOj2oC0FpBtnoxVKLpIcyLw6dkfSil5ANd+hl59W/bpPA8KT90ii1fSNCo3+FcwQVx0YsPznJNA60XfFuVsme7zNcOst6393e1WriZxBanFpfB63zVQc9u1fjyfktx/yiUNxIlre+sz9OCc0AACn94iRhBYh4bbzdleUOTnM7lnD4Dj2FP+xeDIP8CA8wXUeq5+9kopSp2kAmlUEyFUdg4no7naIeu1SZnopfUg56PsVCp9JHiUK1SYAyWbdC+FbUECu5CvI="
}
}

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"Person","id":"https://apfed.club/channel/indio","preferredUsername":"indio","name":"Indio","updated":"2019-08-20T23:52:34Z","icon":{"type":"Image","mediaType":"image/jpeg","updated":"2019-08-20T23:53:37Z","url":"https://apfed.club/photo/profile/l/2","height":300,"width":300},"url":"https://apfed.club/channel/indio","inbox":"https://apfed.club/inbox/indio","outbox":"https://apfed.club/outbox/indio","followers":"https://apfed.club/followers/indio","following":"https://apfed.club/following/indio","endpoints":{"sharedInbox":"https://apfed.club/inbox"},"publicKey":{"id":"https://apfed.club/channel/indio","owner":"https://apfed.club/channel/indio","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"c672a408d2e88b322b36a61bf0c25f586be9245d30293c55b8d653dcc867aaf7","creator":"https://apfed.club/channel/indio/public_key_pem","created":"2019-08-26T07:24:03Z","signatureValue":"MyAv5gnedu6L/DYFaE1TUYvp4LjI9ZUU0axwGYOhgD7qsjivMgwbOrjX/iH32xlcfF8nWOMh/ogu3+Qwr5sqLHkS2AimWmw1+Ubf2KccE58b8vI8zWfyu8QJnMuE92jtBPv8UTQUHw8ZebbExk3L99oXaeyVihKiMBmd63NpVTpGXZTg6m+H+KfWchVajPoyNKZtKMd3nH99x5j54Cqkz0BN5CSTwCSG0wP95G0VtZHtmhX+tsAPM3oAj0d+gtCZSCd8Nu8fvFAwCyTg1oKSfRqKb27EKHlskqK9X57x0jURH77CTAIQSejgGcKJ5GGLtvofubJkafadjagqrtqz6Mz6BZ642ssJ2KGkRAn79Q4F08goI6cfU5lLk2Tooe5A55XERnmE3SkYGyTvLpacZplxJdU0sa+deX9D7+alSGFJZSziaxpCxzrO6lEApe4b9kHXAzn9VaZt9trijkHq/kkq0i3NRcP7n8JG9q+Vv8jY9ddY6HcH89RNCBIA6MKLtAqc+vSc5G24qeZlw2MzlQWBp0KGuVG8DQR00AL6cXLBzF1WY8JZeEg6zqm+DMznbuNzgiS34BP+AehBSHlQ4MZebwDnK3ZPPqGSwioIWMxIFfZDaVDX9Pp1pXAARQMw0c/y4sDcf9FMzsr8jteEa7ZQcoqq5kXQTSCP56TEHnI="}}

View File

@ -15,6 +15,13 @@ test "creating a list" do
assert title == "title" assert title == "title"
end end
test "validates title" do
user = insert(:user)
assert {:error, changeset} = Pleroma.List.create("", user)
assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
end
test "getting a list not belonging to the user" do test "getting a list not belonging to the user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View File

@ -0,0 +1,301 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ModerationLogTest do
alias Pleroma.Activity
alias Pleroma.ModerationLog
use Pleroma.DataCase
import Pleroma.Factory
describe "user moderation" do
setup do
admin = insert(:user, info: %{is_admin: true})
moderator = insert(:user, info: %{is_moderator: true})
subject1 = insert(:user)
subject2 = insert(:user)
[admin: admin, moderator: moderator, subject1: subject1, subject2: subject2]
end
test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
action: "delete"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} deleted user @#{subject1.nickname}"
end
test "logging user creation by moderator", %{
moderator: moderator,
subject1: subject1,
subject2: subject2
} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subjects: [subject1, subject2],
action: "create"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"
end
test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
followed: subject1,
follower: subject2,
action: "follow"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"
end
test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
followed: subject1,
follower: subject2,
action: "unfollow"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"
end
test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
nicknames: [subject1.nickname, subject2.nickname],
tags: ["foo", "bar"],
action: "tag"
})
log = Repo.one(ModerationLog)
users =
[subject1.nickname, subject2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log) ==
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
nicknames: [subject1.nickname, subject2.nickname],
tags: ["foo", "bar"],
action: "untag"
})
log = Repo.one(ModerationLog)
users =
[subject1.nickname, subject2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log) ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
action: "grant",
permission: "moderator"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} made @#{subject1.nickname} moderator"
end
test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
action: "revoke",
permission: "moderator"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"
end
test "logging relay follow", %{moderator: moderator} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_follow",
target: "https://example.org/relay"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} followed relay: https://example.org/relay"
end
test "logging relay unfollow", %{moderator: moderator} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_unfollow",
target: "https://example.org/relay"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
test "logging report update", %{moderator: moderator} do
report = %Activity{
id: "9m9I1F4p8ftrTP6QTI",
data: %{
"type" => "Flag",
"state" => "resolved"
}
}
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "report_update",
subject: report
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"
end
test "logging report response", %{moderator: moderator} do
report = %Activity{
id: "9m9I1F4p8ftrTP6QTI",
data: %{
"type" => "Note"
}
}
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "report_response",
subject: report,
text: "look at this"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"
end
test "logging status sensitivity update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: "true",
visibility: nil
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"
end
test "logging status visibility update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: nil,
visibility: "private"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"
end
test "logging status sensitivity & visibility update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: "true",
visibility: "private"
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"
end
test "logging status deletion", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_delete",
subject_id: note.id
})
log = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log) ==
"@#{moderator.nickname} deleted status ##{note.id}"
end
end
end

View File

@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
import Mock
alias Pleroma.Signature alias Pleroma.Signature
@ -114,4 +115,17 @@ test "it properly deduces the actor id for mastodon and pleroma" do
"https://example.com/users/1234" "https://example.com/users/1234"
end end
end end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end end

View File

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server # Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do defmodule Pleroma.Factory do
@ -143,6 +143,25 @@ def note_activity_factory(attrs \\ %{}) do
|> Map.merge(attrs) |> Map.merge(attrs)
end end
defp expiration_offset_by_minutes(attrs, minutes) do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
|> NaiveDateTime.truncate(:second)
%Pleroma.ActivityExpiration{}
|> Map.merge(attrs)
|> Map.put(:scheduled_at, scheduled_at)
end
def expiration_in_the_past_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, -60)
end
def expiration_in_the_future_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, 61)
end
def article_activity_factory do def article_activity_factory do
article = insert(:article) article = insert(:article)
@ -188,13 +207,15 @@ def like_activity_factory(attrs \\ %{}) do
object = Object.normalize(note_activity) object = Object.normalize(note_activity)
user = insert(:user) user = insert(:user)
data = %{ data =
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), %{
"actor" => user.ap_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Like", "actor" => user.ap_id,
"object" => object.data["id"], "type" => "Like",
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601() "object" => object.data["id"],
} "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{ %Pleroma.Activity{
data: data data: data

View File

@ -17,9 +17,12 @@ def request(
with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do
res res
else else
{_, _r} = error -> error ->
# Logger.warn(r) with {:error, message} <- error do
error Logger.warn(message)
end
{_, _r} = error
end end
end end
@ -772,6 +775,11 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}}
end end
def get("https://apfed.club/channel/indio", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}}
end
def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do
{:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
end end
@ -968,9 +976,25 @@ def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp",
}} }}
end end
def get("http://example.com/rel_me/anchor", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}}
end
def get("http://example.com/rel_me/anchor_nofollow", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}}
end
def get("http://example.com/rel_me/link", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}}
end
def get("http://example.com/rel_me/null", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}}
end
def get(url, query, body, headers) do def get(url, query, body, headers) do
{:error, {:error,
"Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers) inspect(headers)
}"} }"}
end end
@ -1032,7 +1056,10 @@ def post("http://404.site" <> _, _, _, _) do
}} }}
end end
def post(url, _query, _body, _headers) do def post(url, query, body, headers) do
{:error, "Not implemented the mock response for post #{inspect(url)}"} {:error,
"Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end end
end end

View File

@ -50,7 +50,8 @@ test "relay is unfollowed" do
%User{ap_id: follower_id} = local_user = Relay.get_actor() %User{ap_id: follower_id} = local_user = Relay.get_actor()
target_user = User.get_cached_by_ap_id(target_instance) target_user = User.get_cached_by_ap_id(target_instance)
follow_activity = Utils.fetch_latest_follow(local_user, target_user) follow_activity = Utils.fetch_latest_follow(local_user, target_user)
User.follow(local_user, target_user)
assert "#{target_instance}/followers" in refresh_record(local_user).following
Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance])
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
@ -67,6 +68,7 @@ test "relay is unfollowed" do
assert undo_activity.data["type"] == "Undo" assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id assert undo_activity.data["actor"] == local_user.ap_id
assert undo_activity.data["object"] == cancelled_activity.data assert undo_activity.data["object"] == cancelled_activity.data
refute "#{target_instance}/followers" in refresh_record(local_user).following
end end
end end

View File

@ -1269,18 +1269,18 @@ test "preserves hosts in user links text" do
end end
test "Adds rel=me on linkbacked urls" do test "Adds rel=me on linkbacked urls" do
user = insert(:user, ap_id: "http://social.example.org/users/lain") user = insert(:user, ap_id: "https://social.example.org/users/lain")
bio = "http://example.org/rel_me/null" bio = "http://example.com/rel_me/null"
expected_text = "<a href=\"#{bio}\">#{bio}</a>" expected_text = "<a href=\"#{bio}\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user) assert expected_text == User.parse_bio(bio, user)
bio = "http://example.org/rel_me/link" bio = "http://example.com/rel_me/link"
expected_text = "<a href=\"#{bio}\">#{bio}</a>" expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user) assert expected_text == User.parse_bio(bio, user)
bio = "http://example.org/rel_me/anchor" bio = "http://example.com/rel_me/anchor"
expected_text = "<a href=\"#{bio}\">#{bio}</a>" expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user) assert expected_text == User.parse_bio(bio, user)
end end
end end

View File

@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -601,6 +602,34 @@ test "it increases like count when receiving a like action", %{conn: conn} do
end end
end end
describe "/relay/followers" do
test "it returns relay followers", %{conn: conn} do
relay_actor = Relay.get_actor()
user = insert(:user)
User.follow(user, relay_actor)
result =
conn
|> assign(:relay, true)
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> assign(:relay, true)
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
end
end
describe "/users/:nickname/followers" do describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user) user = insert(:user)

View File

@ -21,6 +21,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
:ok :ok
end end
clear_config([:instance, :federating])
describe "streaming out participations" do describe "streaming out participations" do
test "it streams them out" do test "it streams them out" do
user = insert(:user) user = insert(:user)
@ -676,6 +678,29 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end end
describe "like an object" do describe "like an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Pleroma.Web.Federator.publish(like_activity))
end
test "returns exist activity if object already liked" do
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
{:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
assert like_activity == like_activity_exist
end
test "adds a like activity to the db" do test "adds a like activity to the db" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity) assert object = Object.normalize(note_activity)
@ -706,6 +731,25 @@ test "adds a like activity to the db" do
end end
describe "unliking" do describe "unliking" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Pleroma.Web.Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert called(Pleroma.Web.Federator.publish(unlike_activity))
end
test "unliking a previously liked object" do test "unliking a previously liked object" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
object = Object.normalize(note_activity) object = Object.normalize(note_activity)

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
import Pleroma.Factory import Pleroma.Factory
import Mock
test "gets an actor for the relay" do test "gets an actor for the relay" do
user = Relay.get_actor() user = Relay.get_actor()
@ -43,16 +44,21 @@ test "returns activity" do
user = insert(:user) user = insert(:user)
service_actor = Relay.get_actor() service_actor = Relay.get_actor()
ActivityPub.follow(service_actor, user) ActivityPub.follow(service_actor, user)
Pleroma.User.follow(service_actor, user)
assert "#{user.ap_id}/followers" in refresh_record(service_actor).following
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
assert user.ap_id in activity.recipients assert user.ap_id in activity.recipients
assert activity.data["type"] == "Undo" assert activity.data["type"] == "Undo"
assert activity.data["actor"] == service_actor.ap_id assert activity.data["actor"] == service_actor.ap_id
assert activity.data["to"] == [user.ap_id] assert activity.data["to"] == [user.ap_id]
refute "#{user.ap_id}/followers" in refresh_record(service_actor).following
end end
end end
describe "publish/1" do describe "publish/1" do
clear_config([:instance, :federating])
test "returns error when activity not `Create` type" do test "returns error when activity not `Create` type" do
activity = insert(:like_activity) activity = insert(:like_activity)
assert Relay.publish(activity) == {:error, "Not implemented"} assert Relay.publish(activity) == {:error, "Not implemented"}
@ -63,13 +69,44 @@ test "returns error when activity not public" do
assert Relay.publish(activity) == {:error, false} assert Relay.publish(activity) == {:error, false}
end end
test "returns announce activity" do test "returns error when object is unknown" do
activity =
insert(:note_activity,
data: %{
"type" => "Create",
"object" => "http://mastodon.example.org/eee/99541947525187367"
}
)
assert Relay.publish(activity) == {:error, nil}
end
test_with_mock "returns announce activity and publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], true)
service_actor = Relay.get_actor() service_actor = Relay.get_actor()
note = insert(:note_activity) note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce" assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"] assert activity.data["object"] == obj.data["id"]
assert called(Pleroma.Web.Federator.publish(activity))
end
test_with_mock "returns announce activity and not publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], false)
service_actor = Relay.get_actor()
note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"]
refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
end end

View File

@ -19,6 +19,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
end end
describe "handle_incoming" do describe "handle_incoming" do
test "it works for osada follow request" do
user = insert(:user)
data =
File.read!("test/fixtures/osada-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://apfed.club/channel/indio"
assert data["type"] == "Follow"
assert data["id"] == "https://apfed.club/follow/9"
activity = Repo.get(Activity, activity.id)
assert activity.data["state"] == "accept"
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it works for incoming follow requests" do test "it works for incoming follow requests" do
user = insert(:user) user = insert(:user)

View File

@ -564,6 +564,14 @@ test "it works with custom profile fields" do
%{"name" => "foo", "value" => "updated"}, %{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"} %{"name" => "foo1", "value" => "updated"}
] ]
update_data = put_in(update_data, ["object", "attachment"], [])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.Info.fields(user.info) == []
end end
test "it works for incoming update activities which lock the account" do test "it works for incoming update activities which lock the account" do

View File

@ -14,6 +14,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
import Pleroma.Factory import Pleroma.Factory
require Pleroma.Constants
describe "fetch the latest Follow" do describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
@ -87,6 +89,32 @@ test "works with an object that has only IR tags" do
end end
end end
describe "make_unlike_data/3" do
test "returns data for unlike activity" do
user = insert(:user)
like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
assert Utils.make_unlike_data(user, like_activity, nil) == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, like_activity.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"]
}
assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, like_activity.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"],
"id" => "9mJEZK0tky1w2xD2vY"
}
end
end
describe "make_like_data" do describe "make_like_data" do
setup do setup do
user = insert(:user) user = insert(:user)
@ -299,4 +327,78 @@ test "updates the state of the given follow activity" do
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
end end
end end
describe "update_element_in_object/3" do
test "updates likes" do
user = insert(:user)
activity = insert(:note_activity)
object = Object.normalize(activity)
assert {:ok, updated_object} =
Utils.update_element_in_object(
"like",
[user.ap_id],
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "add_like_to_object/2" do
test "add actor to likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note)
assert {:ok, updated_object} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
assert {:ok, updated_object2} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user2.ap_id}},
updated_object
)
assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id]
assert updated_object2.data["like_count"] == 2
end
end
describe "remove_like_from_object/2" do
test "removes ap_id from likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2})
assert {:ok, updated_object} =
Utils.remove_like_from_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user2.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "get_existing_like/2" do
test "fetches existing like" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object)
{:ok, like_activity, _object} = ActivityPub.like(user, object)
assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end
end
end end

View File

@ -7,6 +7,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -24,6 +26,14 @@ test "Delete" do
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
log_entry = Repo.one(ModerationLog)
assert log_entry.data["subject"]["nickname"] == user.nickname
assert log_entry.data["action"] == "delete"
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted user @#{user.nickname}"
assert json_response(conn, 200) == user.nickname assert json_response(conn, 200) == user.nickname
end end
@ -35,12 +45,135 @@ test "Create" do
|> assign(:user, admin) |> assign(:user, admin)
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{ |> post("/api/pleroma/admin/users", %{
"nickname" => "lain", "users" => [
"email" => "lain@example.org", %{
"password" => "test" "nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
}) })
assert json_response(conn, 200) == "lain" response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
assert response == ["success", "success"]
log_entry = Repo.one(ModerationLog)
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Cannot create user with exisiting email" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
}
]
end
test "Cannot create user with exisiting nickname" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => "someuser@plerama.social",
"nickname" => user.nickname
},
"error" => "nickname has already been taken",
"type" => "error"
}
]
end
test "Multiple user creation works in transaction" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
},
%{
"code" => 409,
"data" => %{
"email" => "newuser@pleroma.social",
"nickname" => "newuser"
},
"error" => "",
"type" => "error"
}
]
assert User.get_by_nickname("newuser") === nil
end end
end end
@ -99,6 +232,11 @@ test "allows to force-follow another user" do
follower = User.get_cached_by_id(follower.id) follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user) assert User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end end
end end
@ -122,6 +260,11 @@ test "allows to force-unfollow another user" do
follower = User.get_cached_by_id(follower.id) follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user) refute User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end end
end end
@ -142,17 +285,30 @@ test "allows to force-unfollow another user" do
}&tags[]=foo&tags[]=bar" }&tags[]=foo&tags[]=bar"
) )
%{conn: conn, user1: user1, user2: user2, user3: user3} %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
end end
test "it appends specified tags to users with specified nicknames", %{ test "it appends specified tags to users with specified nicknames", %{
conn: conn, conn: conn,
admin: admin,
user1: user1, user1: user1,
user2: user2 user2: user2
} do } do
assert json_response(conn, :no_content) assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
@ -178,17 +334,30 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user
}&tags[]=x&tags[]=z" }&tags[]=x&tags[]=z"
) )
%{conn: conn, user1: user1, user2: user2, user3: user3} %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
end end
test "it removes specified tags from users with specified nicknames", %{ test "it removes specified tags from users with specified nicknames", %{
conn: conn, conn: conn,
admin: admin,
user1: user1, user1: user1,
user2: user2 user2: user2
} do } do
assert json_response(conn, :no_content) assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == [] assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"] assert User.get_cached_by_id(user2.id).tags == ["y"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["x", "z"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
@ -226,6 +395,11 @@ test "/:right POST, can add to a permission group" do
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"is_admin" => true "is_admin" => true
} }
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user.nickname} admin"
end end
test "/:right DELETE, can remove from a permission group" do test "/:right DELETE, can remove from a permission group" do
@ -241,6 +415,11 @@ test "/:right DELETE, can remove from a permission group" do
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"is_admin" => false "is_admin" => false
} }
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end end
end end
@ -253,10 +432,10 @@ test "/:right DELETE, can remove from a permission group" do
|> assign(:user, admin) |> assign(:user, admin)
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
%{conn: conn} %{conn: conn, admin: admin}
end end
test "deactivates the user", %{conn: conn} do test "deactivates the user", %{conn: conn, admin: admin} do
user = insert(:user) user = insert(:user)
conn = conn =
@ -266,9 +445,14 @@ test "deactivates the user", %{conn: conn} do
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
assert user.info.deactivated == true assert user.info.deactivated == true
assert json_response(conn, :no_content) assert json_response(conn, :no_content)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}"
end end
test "activates the user", %{conn: conn} do test "activates the user", %{conn: conn, admin: admin} do
user = insert(:user, info: %{deactivated: true}) user = insert(:user, info: %{deactivated: true})
conn = conn =
@ -278,6 +462,11 @@ test "activates the user", %{conn: conn} do
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
assert user.info.deactivated == false assert user.info.deactivated == false
assert json_response(conn, :no_content) assert json_response(conn, :no_content)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} activated user @#{user.nickname}"
end end
test "returns 403 when requested by a non-admin", %{conn: conn} do test "returns 403 when requested by a non-admin", %{conn: conn} do
@ -868,6 +1057,11 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname)
} }
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}"
end end
describe "GET /api/pleroma/admin/users/invite_token" do describe "GET /api/pleroma/admin/users/invite_token" do
@ -1053,25 +1247,35 @@ test "returns 404 when report id is invalid", %{conn: conn} do
"status_ids" => [activity.id] "status_ids" => [activity.id]
}) })
%{conn: assign(conn, :user, admin), id: report_id} %{conn: assign(conn, :user, admin), id: report_id, admin: admin}
end end
test "mark report as resolved", %{conn: conn, id: id} do test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
response = response =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|> json_response(:ok) |> json_response(:ok)
assert response["state"] == "resolved" assert response["state"] == "resolved"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
end end
test "closes report", %{conn: conn, id: id} do test "closes report", %{conn: conn, id: id, admin: admin} do
response = response =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|> json_response(:ok) |> json_response(:ok)
assert response["state"] == "closed" assert response["state"] == "closed"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'closed' state"
end end
test "returns 400 when state is unknown", %{conn: conn, id: id} do test "returns 400 when state is unknown", %{conn: conn, id: id} do
@ -1202,14 +1406,15 @@ test "returns 403 when requested by anonymous" do
end end
end end
#
describe "POST /api/pleroma/admin/reports/:id/respond" do describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)} %{conn: assign(conn, :user, admin), admin: admin}
end end
test "returns created dm", %{conn: conn} do test "returns created dm", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user) [reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user) activity = insert(:note_activity, user: target_user)
@ -1232,6 +1437,13 @@ test "returns created dm", %{conn: conn} do
assert reporter.nickname in recipients assert reporter.nickname in recipients
assert response["content"] == "I will check it out" assert response["content"] == "I will check it out"
assert response["visibility"] == "direct" assert response["visibility"] == "direct"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} responded with 'I will check it out' to report ##{
response["id"]
}"
end end
test "returns 400 when status is missing", %{conn: conn} do test "returns 400 when status is missing", %{conn: conn} do
@ -1255,10 +1467,10 @@ test "returns 404 when report id is invalid", %{conn: conn} do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity) activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id} %{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
end end
test "toggle sensitive flag", %{conn: conn, id: id} do test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
response = response =
conn conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
@ -1266,6 +1478,11 @@ test "toggle sensitive flag", %{conn: conn, id: id} do
assert response["sensitive"] assert response["sensitive"]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set sensitive: 'true'"
response = response =
conn conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
@ -1274,7 +1491,7 @@ test "toggle sensitive flag", %{conn: conn, id: id} do
refute response["sensitive"] refute response["sensitive"]
end end
test "change visibility flag", %{conn: conn, id: id} do test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response = response =
conn conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"}) |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
@ -1282,6 +1499,11 @@ test "change visibility flag", %{conn: conn, id: id} do
assert response["visibility"] == "public" assert response["visibility"] == "public"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set visibility: 'public'"
response = response =
conn conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"}) |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
@ -1311,15 +1533,20 @@ test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity) activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id} %{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
end end
test "deletes status", %{conn: conn, id: id} do test "deletes status", %{conn: conn, id: id, admin: admin} do
conn conn
|> delete("/api/pleroma/admin/statuses/#{id}") |> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok) |> json_response(:ok)
refute Activity.get_by_id(id) refute Activity.get_by_id(id)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted status ##{id}"
end end
test "returns error when status is not exist", %{conn: conn} do test "returns error when status is not exist", %{conn: conn} do
@ -2020,6 +2247,108 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do
assert json_response(conn, 200) |> length() == 5 assert json_response(conn, 200) |> length() == 5
end end
end end
describe "GET /api/pleroma/admin/moderation_log" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin), admin: admin}
end
test "returns the log", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn = get(conn, "/api/pleroma/admin/moderation_log")
response = json_response(conn, 200)
[first_entry, second_entry] = response
assert response |> length() == 2
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "returns the log with pagination", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
response1 = json_response(conn1, 200)
[first_entry] = response1
assert response1 |> length() == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
response2 = json_response(conn2, 200)
[second_entry] = response2
assert response2 |> length() == 1
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
end
end end
# Needed for testing # Needed for testing

View File

@ -204,6 +204,21 @@ test "it returns error when character limit is exceeded" do
assert {:error, "The status is over the character limit"} = assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{"status" => "foobar"}) CommonAPI.post(user, %{"status" => "foobar"})
end end
test "it can handle activities that expire" do
user = insert(:user)
expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.truncate(:second)
|> NaiveDateTime.add(1_000_000, :second)
assert {:ok, activity} =
CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000})
assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id)
assert expiration.scheduled_at == expires_at
end
end end
describe "reactions" do describe "reactions" do

View File

@ -0,0 +1,166 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Repo
import Pleroma.Factory
test "creating a list", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
test "renders error for invalid params", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => nil})
assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
end
test "listing a user's lists", %{conn: conn} do
user = insert(:user)
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cofe"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists")
assert [
%{"id" => _, "title" => "cofe"},
%{"id" => _, "title" => "cuties"}
] = json_response(conn, :ok)
end
test "adding users to a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
test "removing users from a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
test "listing users in a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "retrieving a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(list.id)
end
test "renders 404 if list is not found", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/666")
assert %{"error" => "List not found"} = json_response(conn, :not_found)
end
test "renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
test "validates title when renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => " "})
assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
end
test "deleting a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
end

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -151,6 +152,32 @@ test "posting a status", %{conn: conn} do
assert %{"id" => third_id} = json_response(conn_three, 200) assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_id refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 120 * 60
conn_four =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
assert activity = Activity.get_by_id(fourth_id)
assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
estimated_expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.truncate(:second)
# This assert will fail if the test takes longer than a minute. I sure hope it never does:
assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
assert fourth_response["pleroma"]["expires_at"] ==
NaiveDateTime.to_iso8601(expiration.scheduled_at)
end end
test "replying to a status", %{conn: conn} do test "replying to a status", %{conn: conn} do
@ -404,7 +431,7 @@ test "direct timeline", %{conn: conn} do
assert %{"visibility" => "direct"} = status assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"] assert status["url"] != direct.data["id"]
# User should be able to see his own direct message # User should be able to see their own direct message
res_conn = res_conn =
build_conn() build_conn()
|> assign(:user, user_one) |> assign(:user, user_one)
@ -901,106 +928,7 @@ test "delete a filter", %{conn: conn} do
end end
end end
describe "lists" do describe "list timelines" do
test "creating a list", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
test "adding users to a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
test "removing users from a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
test "listing users in a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "retrieving a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(list.id)
end
test "renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
test "deleting a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
test "list timeline", %{conn: conn} do test "list timeline", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View File

@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.ListView
test "Represent a list" do test "show" do
user = insert(:user) user = insert(:user)
title = "mortal enemies" title = "mortal enemies"
{:ok, list} = Pleroma.List.create(title, user) {:ok, list} = Pleroma.List.create(title, user)
@ -17,6 +17,16 @@ test "Represent a list" do
title: title title: title
} }
assert expected == ListView.render("list.json", %{list: list}) assert expected == ListView.render("show.json", %{list: list})
end
test "index" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("my list", user)
{:ok, list2} = Pleroma.List.create("cofe", user)
assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] =
ListView.render("index.json", lists: [list, list2])
end end
end end

View File

@ -149,6 +149,7 @@ test "a note activity" do
in_reply_to_account_acct: nil, in_reply_to_account_acct: nil,
content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
expires_at: nil,
direct_conversation_id: nil direct_conversation_id: nil
} }
} }

View File

@ -5,33 +5,8 @@
defmodule Pleroma.Web.RelMeTest do defmodule Pleroma.Web.RelMeTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
setup do setup_all do
Tesla.Mock.mock(fn Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
%{
method: :get,
url: "http://example.com/rel_me/anchor"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}
%{
method: :get,
url: "http://example.com/rel_me/anchor_nofollow"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}
%{
method: :get,
url: "http://example.com/rel_me/link"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}
%{
method: :get,
url: "http://example.com/rel_me/null"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}
end)
:ok :ok
end end