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

develop
Rachel Fae Fox (foxiepaws) 2019-09-02 13:16:01 -04:00
commit db3dae189b
130 changed files with 4123 additions and 1084 deletions

View File

@ -18,10 +18,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- NodeInfo: Return `mailerEnabled` in `metadata` - NodeInfo: Return `mailerEnabled` in `metadata`
- Mastodon API: Unsubscribe followers when they unfollow a user - Mastodon API: Unsubscribe followers when they unfollow a user
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
### 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
- Metadata rendering errors resulting in the entire page being inaccessible - Metadata rendering errors resulting in the entire page being inaccessible
- `federation_incoming_replies_max_depth` option being ignored in certain cases - `federation_incoming_replies_max_depth` option being ignored in certain cases
- Federation/MediaProxy not working with instances that have wrong certificate order - Federation/MediaProxy not working with instances that have wrong certificate order
@ -43,9 +46,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected. - Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
- Report email not being sent to admins when the reporter is a remote user - Report email not being sent to admins when the reporter is a remote user
- MRF: ensure that subdomain_match calls are case-insensitive - MRF: ensure that subdomain_match calls are case-insensitive
- 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
- 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.
@ -67,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `pleroma.deactivated` to the Account entity - Mastodon API: Add `pleroma.deactivated` to the Account entity
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit. - Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id - Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
- Mastodon API: Improve support for the user profile custom fields
- Admin API: Return users' tags when querying reports - Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users - Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID - Admin API: Allow querying user by ID
@ -85,6 +96,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
@ -186,6 +198,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

@ -255,6 +255,10 @@ config :pleroma, :instance,
dynamic_configuration: false, dynamic_configuration: false,
user_bio_length: 5000, user_bio_length: 5000,
user_name_length: 100, user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 512,
external_user_synchronization: true external_user_synchronization: true
config :pleroma, :markup, config :pleroma, :markup,
@ -452,6 +456,7 @@ config :pleroma, Pleroma.Web.Federator.RetryQueue,
max_retries: 5 max_retries: 5
config :pleroma_job_queue, :queues, config :pleroma_job_queue, :queues,
activity_expiration: 10,
federator_incoming: 50, federator_incoming: 50,
federator_outgoing: 50, federator_outgoing: 50,
web_push: 50, web_push: 50,
@ -512,6 +517,17 @@ config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
config :pleroma, Pleroma.Emails.UserEmail,
logo: nil,
styling: %{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics" config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
config :pleroma, Pleroma.ScheduledActivity, config :pleroma, Pleroma.ScheduledActivity,
@ -540,15 +556,9 @@ config :pleroma, :env, Mix.env()
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}
# 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

@ -77,7 +77,8 @@ config :pleroma, Pleroma.ScheduledActivity,
config :pleroma, :rate_limit, config :pleroma, :rate_limit,
search: [{1000, 30}, {1000, 30}], search: [{1000, 30}, {1000, 30}],
app_account_creation: {10_000, 5}, app_account_creation: {10_000, 5},
password_reset: {1000, 30} password_reset: {1000, 30},
ap_routes: nil
config :pleroma, :http_security, report_uri: "https://endpoint.com" config :pleroma, :http_security, report_uri: "https://endpoint.com"
@ -91,11 +92,10 @@ config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp3
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`.
@ -132,6 +132,10 @@ config :pleroma, Pleroma.Emails.Mailer,
* `skip_thread_containment`: Skip filter out broken threads. The default is `false`. * `skip_thread_containment`: Skip filter out broken threads. The default is `false`.
* `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`. * `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`.
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api. * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
* `max_account_fields`: The maximum number of custom fields in the user profile (default: `10`)
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`)
* `account_field_name_length`: An account field name maximum length (default: `512`)
* `account_field_value_length`: An account field value maximum length (default: `512`)
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users. * `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
@ -491,6 +495,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
@ -555,6 +563,11 @@ Email notifications settings.
- interval: Minimum interval between digest emails to one user - interval: Minimum interval between digest emails to one user
- inactivity_threshold: Minimum user inactivity threshold - inactivity_threshold: Minimum user inactivity threshold
## Pleroma.Emails.UserEmail
- `:logo` - a path to a custom logo. Set it to `nil` to use the default Pleroma logo.
- `:styling` - a map with color settings for email templates.
## OAuth consumer mode ## OAuth consumer mode
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
@ -658,6 +671,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

@ -27,7 +27,15 @@ defmodule Mix.Tasks.Pleroma.Digest do
patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at} patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at}
_user = Pleroma.DigestEmailWorker.perform(patched_user) with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") {:ok, _} = Pleroma.Emails.Mailer.deliver(email)
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})")
else
_ ->
Mix.shell().info(
"Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}"
)
end
end end
end end

View File

@ -53,13 +53,11 @@ defmodule Mix.Tasks.Pleroma.Relay do
def run(["list"]) do def run(["list"]) do
start_pleroma() start_pleroma()
with %User{} = user <- Relay.get_actor() do with %User{following: following} = _user <- Relay.get_actor() do
user.following following
|> Enum.each(fn entry -> |> Enum.map(fn entry -> URI.parse(entry).host end)
URI.parse(entry) |> Enum.uniq()
|> Map.get(:host) |> Enum.each(&shell_info(&1))
|> shell_info()
end)
else else
e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end end

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,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityExpirationWorker do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
require Logger
use GenServer
import Ecto.Query
@schedule_interval :timer.minutes(1)
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end
@impl true
def init(_) do
if Config.get([ActivityExpiration, :enabled]) do
schedule_next()
{:ok, nil}
else
:ignore
end
end
def perform(:execute, expiration_id) do
try do
expiration =
ActivityExpiration
|> where([e], e.id == ^expiration_id)
|> Repo.one!()
activity = Activity.get_by_id_with_object(expiration.activity_id)
user = User.get_by_ap_id(activity.object.data["actor"])
CommonAPI.delete(activity.id, user)
rescue
error ->
Logger.error("#{__MODULE__} Couldn't delete expired activity: #{inspect(error)}")
end
end
@impl true
def handle_info(:perform, state) do
ActivityExpiration.due_expirations(@schedule_interval)
|> Enum.each(fn expiration ->
PleromaJobQueue.enqueue(:activity_expiration, __MODULE__, [:execute, expiration.id])
end)
schedule_next()
{:noreply, state}
end
defp schedule_next do
Process.send_after(self(), :perform, @schedule_interval)
end
end

View File

@ -35,7 +35,8 @@ defmodule Pleroma.Application 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

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DigestEmailWorker do defmodule Pleroma.DigestEmailWorker do
import Ecto.Query import Ecto.Query

View File

@ -7,21 +7,21 @@ defmodule Pleroma.Emails.UserEmail do
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email} use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router alias Pleroma.Web.Router
defp instance_config, do: Pleroma.Config.get(:instance) defp instance_name, do: Config.get([:instance, :name])
defp instance_name, do: instance_config()[:name]
defp sender do defp sender do
email = Keyword.get(instance_config(), :notify_email, instance_config()[:email]) email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
{instance_name(), email} {instance_name(), email}
end end
defp recipient(email, nil), do: email defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email} defp recipient(email, name), do: {name, email}
defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name) defp recipient(%User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, token) when is_binary(token) do def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token) password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
@ -93,67 +93,86 @@ defmodule Pleroma.Emails.UserEmail do
Includes Mentions and New Followers data Includes Mentions and New Followers data
If there are no mentions (even when new followers exist), the function will return nil If there are no mentions (even when new followers exist), the function will return nil
""" """
@spec digest_email(Pleroma.User.t()) :: Swoosh.Email.t() | nil @spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do def digest_email(user) do
new_notifications = notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
|> Enum.reduce(%{followers: [], mentions: []}, fn
%{activity: %{data: %{"type" => "Create"}, actor: actor} = activity} = notification,
acc ->
new_mention = %{
data: notification,
object: Pleroma.Object.normalize(activity),
from: Pleroma.User.get_by_ap_id(actor)
}
%{acc | mentions: [new_mention | acc.mentions]} mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity)
object = update_in(object.data["content"], &format_links/1)
%{activity: %{data: %{"type" => "Follow"}, actor: actor} = activity} = notification, %{
acc -> data: notification,
new_follower = %{ object: object,
data: notification, from: User.get_by_ap_id(notification.activity.actor)
object: Pleroma.Object.normalize(activity), }
from: Pleroma.User.get_by_ap_id(actor)
}
%{acc | followers: [new_follower | acc.followers]}
_, acc ->
acc
end) end)
with [_ | _] = mentions <- new_notifications.mentions do followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity),
from: User.get_by_ap_id(notification.activity.actor)
}
end)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
html_data = %{ html_data = %{
instance: instance_name(), instance: instance_name(),
user: user, user: user,
mentions: mentions, mentions: mentions,
followers: new_notifications.followers, followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest") unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
} }
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.png")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
new() new()
|> to(recipient(user)) |> to(recipient(user))
|> from(sender()) |> from(sender())
|> subject("Your digest from #{instance_name()}") |> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data) |> render_body("digest.html", html_data)
else |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline))
_ ->
nil
end end
end end
defp format_links(str) do
re = ~r/<a.+href=['"].*>/iU
%{link_color: color} = Config.get([__MODULE__, :styling])
Regex.replace(re, str, fn link ->
String.replace(link, "<a", "<a style=\"color: #{color};text-decoration: none;\"")
end)
end
@doc """ @doc """
Generate unsubscribe link for given user and notifications type. Generate unsubscribe link for given user and notifications type.
The link contains JWT token with the data, and subscription can be modified without The link contains JWT token with the data, and subscription can be modified without
authorization. authorization.
""" """
@spec unsubscribe_url(Pleroma.User.t(), String.t()) :: String.t() @spec unsubscribe_url(User.t(), String.t()) :: String.t()
def unsubscribe_url(user, notifications_type) do def unsubscribe_url(user, notifications_type) do
token = token =
%{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false} %{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false}
|> Pleroma.JWT.generate_and_sign!() |> Pleroma.JWT.generate_and_sign!()
|> Base.encode64() |> Base.encode64()
Router.Helpers.subscription_url(Pleroma.Web.Endpoint, :unsubscribe, token) Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
end end
end end

View File

@ -282,3 +282,31 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
def scrub({_tag, children}), do: children def scrub({_tag, children}), do: children
def scrub(text), do: text def scrub(text), do: text
end end
defmodule Pleroma.HTML.Scrubber.LinksOnly do
@moduledoc """
An HTML scrubbing policy which limits to links only.
"""
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
# links
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer",
"me"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.strip_everything_not_covered()
end

View File

@ -109,15 +109,19 @@ defmodule Pleroma.List 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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.Object.Fetcher 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

@ -109,7 +109,11 @@ defmodule Pleroma.ReverseProxy do
end end
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts), with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do :ok <-
header_length_constraint(
headers,
Keyword.get(opts, :max_body_length, @max_body_length)
) do
response(conn, client, url, code, headers, opts) response(conn, client, url, code, headers, opts)
else else
{:ok, code, headers} -> {:ok, code, headers} ->
@ -200,7 +204,11 @@ defmodule Pleroma.ReverseProxy do
{:ok, data} <- client().stream_body(client), {:ok, data} <- client().stream_body(client),
{:ok, duration} <- increase_read_duration(duration), {:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data), sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), :ok <-
body_size_constraint(
sent_so_far,
Keyword.get(opts, :max_body_length, @max_body_length)
),
{:ok, conn} <- chunk(conn, data) do {:ok, conn} <- chunk(conn, data) do
chunk_reply(conn, client, opts, sent_so_far, duration) chunk_reply(conn, client, opts, sent_so_far, duration)
else else

View File

@ -53,4 +53,10 @@ defmodule Pleroma.Signature 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

@ -222,12 +222,12 @@ defmodule Pleroma.User do
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
end end
def upgrade_changeset(struct, params \\ %{}) do def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
info_cng = User.Info.user_upgrade(struct.info, params[:info]) info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
struct struct
|> cast(params, [ |> cast(params, [
@ -330,7 +330,13 @@ defmodule Pleroma.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
@ -750,6 +756,7 @@ defmodule Pleroma.User do
|> update_and_set_cache() |> update_and_set_cache()
end end
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do with {:ok, user} <- fetch_follow_information(user) do
user user
@ -807,9 +814,10 @@ defmodule Pleroma.User do
end end
end end
@spec maybe_update_following_count(User.t()) :: User.t()
def maybe_update_following_count(%User{local: false} = user) do def maybe_update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do if Pleroma.Config.get([:instance, :external_user_synchronization]) do
{:ok, maybe_fetch_follow_information(user)} maybe_fetch_follow_information(user)
else else
user user
end end

View File

@ -49,6 +49,8 @@ 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: nil)
field(:raw_fields, {:array, :map}, default: [])
field(:notification_settings, :map, field(:notification_settings, :map,
default: %{ default: %{
@ -254,11 +256,13 @@ defmodule Pleroma.User.Info do
:hide_followers, :hide_followers,
:hide_follows, :hide_follows,
:follower_count, :follower_count,
:fields,
:following_count :following_count
]) ])
|> validate_fields(true)
end end
def user_upgrade(info, params) do def user_upgrade(info, params, remote? \\ false) do
info info
|> cast(params, [ |> cast(params, [
:ap_enabled, :ap_enabled,
@ -269,8 +273,10 @@ defmodule Pleroma.User.Info do
:follower_count, :follower_count,
:following_count, :following_count,
:hide_follows, :hide_follows,
:fields,
:hide_followers :hide_followers
]) ])
|> validate_fields(remote?)
end end
def profile_update(info, params) do def profile_update(info, params) do
@ -286,10 +292,40 @@ defmodule Pleroma.User.Info do
:background, :background,
:show_role, :show_role,
:skip_thread_containment, :skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store :pleroma_settings_store
]) ])
|> validate_fields()
end end
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
|> validate_change(:fields, fn :fields, fields ->
if Enum.all?(fields, &valid_field?/1) do
[]
else
[fields: "invalid"]
end
end)
end
defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
is_binary(name) &&
is_binary(value) &&
String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
defp valid_field?(_), do: false
@spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t() @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
def confirmation_changeset(info, opts) do def confirmation_changeset(info, opts) do
need_confirmation? = Keyword.get(opts, :need_confirmation) need_confirmation? = Keyword.get(opts, :need_confirmation)
@ -384,6 +420,21 @@ defmodule Pleroma.User.Info do
cast(info, params, [:muted_reblogs]) cast(info, params, [:muted_reblogs])
end end
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
attachment
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|> Enum.take(limit)
end
def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields
def follow_information_update(info, params) do def follow_information_update(info, params) do
info info
|> cast(params, [ |> cast(params, [

View File

@ -65,12 +65,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if not is_nil(actor) do if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor), with user <- User.get_cached_by_ap_id(actor),
false <- user.info.deactivated do false <- user.info.deactivated do
:ok true
else else
_e -> :reject _e -> false
end end
else else
:ok true
end end
end end
@ -119,10 +119,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(_create_data), do: :noop def increase_poll_votes_if_vote(_create_data), do: :noop
def insert(map, local \\ true, fake \\ false) when is_map(map) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map), with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake), map <- lazy_put_activity_defaults(map, fake),
:ok <- check_actor_is_active(map["actor"]), true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)}, {_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map), {:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map), {recipients, _, _} = get_recipients(map),
@ -139,7 +139,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# 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
@ -331,12 +331,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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),
@ -411,7 +406,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"actor" => ap_id, "actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id} "object" => %{"type" => "Person", "id" => ap_id}
}, },
{:ok, activity} <- insert(data, true, true), {:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, user} {:ok, user}
end end
@ -1023,6 +1018,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"url" => [%{"href" => data["image"]["url"]}] "url" => [%{"href" => data["image"]["url"]}]
} }
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
locked = data["manuallyApprovesFollowers"] || false locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
@ -1032,6 +1033,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
ap_enabled: true, ap_enabled: true,
source_data: data, source_data: data,
banner: banner, banner: banner,
fields: fields,
locked: locked locked: locked
}, },
avatar: avatar, avatar: avatar,

View File

@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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(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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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) 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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(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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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) 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
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 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
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 @@ defmodule Pleroma.Web.ActivityPub.Relay 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 @@ defmodule Pleroma.Web.ActivityPub.Relay 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 @@ defmodule Pleroma.Web.ActivityPub.Relay 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

@ -464,8 +464,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"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},
@ -598,14 +600,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
banner = new_user_data[:info][:banner] banner = new_user_data[:info][:banner]
locked = new_user_data[:info][:locked] || false locked = new_user_data[:info][:locked] || false
attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
fields =
attachment
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
update_data = update_data =
new_user_data new_user_data
|> Map.take([:name, :bio, :avatar]) |> Map.take([:name, :bio, :avatar])
|> Map.put(:info, %{banner: banner, locked: locked}) |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
actor actor
|> User.upgrade_changeset(update_data) |> User.upgrade_changeset(update_data, true)
|> User.update_and_set_cache() |> User.update_and_set_cache()
ActivityPub.update(%{ ActivityPub.update(%{

View File

@ -166,6 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Utils 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
priority = priority =
@ -256,46 +257,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@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,
@ -315,7 +297,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> 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,
@ -323,38 +305,49 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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
@ -405,7 +398,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%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],
@ -413,10 +406,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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
@ -478,7 +468,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity_id, activity_id,
false false
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -486,8 +476,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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(
@ -496,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity_id, activity_id,
true true
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -504,8 +493,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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 """
@ -516,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%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,
@ -524,8 +512,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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(
@ -533,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%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,
@ -541,8 +528,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"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(
@ -573,14 +559,13 @@ defmodule Pleroma.Web.ActivityPub.Utils 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
@ -610,25 +595,23 @@ defmodule Pleroma.Web.ActivityPub.Utils 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
@ -799,4 +782,7 @@ defmodule Pleroma.Web.ActivityPub.Utils 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

@ -80,6 +80,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Transmogrifier.add_emoji_tags() |> Transmogrifier.add_emoji_tags()
|> Map.get("tag", []) |> Map.get("tag", [])
fields =
user.info
|> User.Info.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Person", "type" => "Person",
@ -98,6 +109,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
"endpoints" => endpoints, "endpoints" => endpoints,
"attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ user_tags "tag" => (user.info.source_data["tag"] || []) ++ user_tags
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))

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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
{: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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AccountView 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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"])
@ -138,7 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
user_info_emojis = user_info_emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) user.info
|> Map.get(:emoji, [])
|> Enum.concat(Formatter.get_emoji_map(emojis_text))
|> Enum.dedup() |> Enum.dedup()
info_params = info_params =
@ -157,6 +159,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end) end)
end) end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "fields", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields}
end)
|> add_if_present(params, "fields", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)} {:ok, Map.merge(user.info.pleroma_settings_store, value)}
end) end)
@ -181,7 +189,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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)
@ -217,7 +225,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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)
@ -229,7 +237,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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
@ -241,7 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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
@ -251,7 +259,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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
@ -798,8 +806,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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)
@ -1197,88 +1205,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 =
@ -1412,8 +1344,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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
@ -1477,7 +1409,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, app} {:ok, app}
else else
app app
|> Ecto.Changeset.change(%{scopes: scopes}) |> Changeset.change(%{scopes: scopes})
|> Repo.update() |> Repo.update()
end end
@ -1579,35 +1511,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController 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

@ -94,12 +94,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end) end)
fields = fields =
(user.info.source_data["attachment"] || []) user.info
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> User.Info.fields()
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) |> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
raw_fields = Map.get(user.info, :raw_fields, [])
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for])) bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user}) relationship = render("relationship.json", %{user: opts[:for], target: user})
%{ %{
@ -124,6 +130,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
source: %{ source: %{
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")), note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false, sensitive: false,
fields: raw_fields,
pleroma: %{} pleroma: %{}
}, },

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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
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

@ -22,6 +22,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Router alias Pleroma.Web.Router
alias Pleroma.Web.XML alias Pleroma.Web.XML
plug(
Pleroma.Plugs.RateLimiter,
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
)
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
plug( plug(
@ -32,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

@ -1,20 +1,568 @@
<h1>Hey <%= @user.nickname %>, here is what you've missed!</h1> <!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<h2>New Mentions:</h2> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
<ul> xmlns:v="urn:schemas-microsoft-com:vml">
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
<li><%= link from.nickname, to: mention.activity.actor %>: <%= raw object.data["content"] %></li>
<% end %>
</ul>
<%= if @followers != [] do %> <head>
<h2><%= length(@followers) %> New Followers:</h2> <!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<ul> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<%= for %{data: follow, from: from} <- @followers do %> <meta content="width=device-width" name="viewport" />
<li><%= link from.nickname, to: follow.activity.actor %></li> <!--[if !mso]><!-->
<% end %> <meta content="IE=edge" http-equiv="X-UA-Compatible" />
</ul> <!--<![endif]-->
<% end %> <title><%= @email.subject %><</title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
<p>You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</p> a {
<p>The email address you are subscribed as is <%= @user.email %>. To unsubscribe, please go <%= link "here", to: @unsubscribe_link %>.</p>
color: <%= @styling.link_color %>;
text-decoration: none;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 610px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: <%= @styling.background_color %>;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="<%= @styling.background_color %>" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: <%= @styling.background_color %>; width: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:logo.png"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
<p style="line-height: 36px; text-align: center; margin: 0;"><span
style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 24px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 20px;">Mentions</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
<%# mention START %>
<%# user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num3"
style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
<!--<![endif]-->
<div align="left" class="img-container left "
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
title="<%= from.name %>" width="76" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num9"
style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: mention.activity.actor %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# user card END %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<span style="font-size: 16px; line-height: 19px;"><%= raw object.data["content"] %></span></div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 15px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_muted_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:15px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_muted_color %>;">
<p style="font-size: 14px; line-height: 16px; margin: 0;"><%= format_date object.data["published"] %></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# mention END %>
<% end %>
<%= if @followers != [] do %>
<%# new followers header START %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 12px; line-height: 24px; text-align: center; margin: 0;"><span
style="font-size: 20px;"><%= length(@followers) %> New Followers</span><span
style="font-size: 20px; line-height: 24px;"></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# new followers header END %>
<%= for %{data: follow, from: from} <- @followers do %>
<%# user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num3"
style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
<!--<![endif]-->
<div align="left" class="img-container left "
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
title="<%= from.name %>" width="76" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num9"
style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: follow.activity.actor %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# user card END %>
<% end %>
<% end %>
<%# divider start %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# divider end %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
<p
style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 </p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View File

@ -74,12 +74,15 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|> HTML.filter_tags(User.html_filter_policy(for_user)) |> HTML.filter_tags(User.html_filter_policy(for_user))
|> Formatter.emojify(emoji) |> Formatter.emojify(emoji)
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields = fields =
(user.info.source_data["attachment"] || []) user.info
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> User.Info.fields()
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) |> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
data = data =
%{ %{

View File

@ -2,4 +2,14 @@ defmodule Pleroma.Web.EmailView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML import Phoenix.HTML
import Phoenix.HTML.Link import Phoenix.HTML.Link
def avatar_url(user) do
Pleroma.User.avatar_url(user)
end
def format_date(date) when is_binary(date) do
date
|> Timex.parse!("{ISO:Extended:Z}")
|> Timex.format!("{Mshort} {D}, {YYYY} {h24}:{m}")
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,7 @@
defmodule Pleroma.Repo.Migrations.AddLikesIndexToObjects do
use Ecto.Migration
def change do
create_if_not_exists index(:objects, ["(data->'likes')"], using: :gin, name: :objects_likes)
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

@ -164,4 +164,13 @@ defmodule Pleroma.ActivityTest do
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

@ -5,14 +5,8 @@
defmodule Pleroma.Config.TransferTaskTest do defmodule Pleroma.Config.TransferTaskTest do
use Pleroma.DataCase use Pleroma.DataCase
setup do clear_config([:instance, :dynamic_configuration]) do
dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
Pleroma.Config.put([:instance, :dynamic_configuration], true) Pleroma.Config.put([:instance, :dynamic_configuration], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
end)
end end
test "transfer config values from db to env" do test "transfer config values from db to env" do

View File

@ -11,14 +11,8 @@ defmodule Pleroma.ConversationTest do
import Pleroma.Factory import Pleroma.Factory
setup_all do clear_config_all([:instance, :federating]) do
config_path = [:instance, :federating] Pleroma.Config.put([:instance, :federating], true)
initial_setting = Pleroma.Config.get(config_path)
Pleroma.Config.put(config_path, true)
on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
:ok
end end
test "it goes through old direct conversations" do test "it goes through old direct conversations" do

View File

@ -15,11 +15,7 @@ defmodule Pleroma.Emails.MailerTest do
to: [{"Test User", "user1@example.com"}] to: [{"Test User", "user1@example.com"}]
} }
setup do clear_config([Pleroma.Emails.Mailer, :enabled])
value = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
on_exit(fn -> Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], value) end)
:ok
end
test "not send email when mailer is disabled" do test "not send email when mailer is disabled" do
Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false)

View File

@ -1,10 +1,10 @@
{ {
"type": "Update", "type": "Update",
"object": { "object": {
"url": "http://mastodon.example.org/@gargron", "url": "http://mastodon.example.org/@gargron",
"type": "Person", "type": "Person",
"summary": "<p>Some bio</p>", "summary": "<p>Some bio</p>",
"publicKey": { "publicKey": {
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
"owner": "http://mastodon.example.org/users/gargron", "owner": "http://mastodon.example.org/users/gargron",
"id": "http://mastodon.example.org/users/gargron#main-key" "id": "http://mastodon.example.org/users/gargron#main-key"
@ -20,7 +20,27 @@
"endpoints": { "endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox" "sharedInbox": "http://mastodon.example.org/inbox"
}, },
"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"} "attachment": [{
"type": "PropertyValue",
"name": "foo",
"value": "updated"
},
{
"type": "PropertyValue",
"name": "foo1",
"value": "updated"
}
],
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
},
"image": {
"type": "Image",
"mediaType": "image/png",
"url": "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
}, },
"id": "http://mastodon.example.org/users/gargron#updates/1519563538", "id": "http://mastodon.example.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron", "actor": "http://mastodon.example.org/users/gargron",

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

@ -1 +1,54 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":null,"summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}} {
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji"
}],
"id": "http://mastodon.example.org/users/admin",
"type": "Person",
"following": "http://mastodon.example.org/users/admin/following",
"followers": "http://mastodon.example.org/users/admin/followers",
"inbox": "http://mastodon.example.org/users/admin/inbox",
"outbox": "http://mastodon.example.org/users/admin/outbox",
"preferredUsername": "admin",
"name": null,
"summary": "\u003cp\u003e\u003c/p\u003e",
"url": "http://mastodon.example.org/@admin",
"manuallyApprovesFollowers": false,
"publicKey": {
"id": "http://mastodon.example.org/users/admin#main-key",
"owner": "http://mastodon.example.org/users/admin",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"attachment": [{
"type": "PropertyValue",
"name": "foo",
"value": "bar"
},
{
"type": "PropertyValue",
"name": "foo1",
"value": "bar1"
}
],
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
},
"image": {
"type": "Image",
"mediaType": "image/png",
"url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
}

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

@ -4,21 +4,19 @@
defmodule Pleroma.HTTP.RequestBuilderTest do defmodule Pleroma.HTTP.RequestBuilderTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
use Pleroma.Tests.Helpers
alias Pleroma.HTTP.RequestBuilder alias Pleroma.HTTP.RequestBuilder
describe "headers/2" do describe "headers/2" do
clear_config([:http, :send_user_agent])
test "don't send pleroma user agent" do test "don't send pleroma user agent" do
assert RequestBuilder.headers(%{}, []) == %{headers: []} assert RequestBuilder.headers(%{}, []) == %{headers: []}
end end
test "send pleroma user agent" do test "send pleroma user agent" do
send = Pleroma.Config.get([:http, :send_user_agent])
Pleroma.Config.put([:http, :send_user_agent], true) Pleroma.Config.put([:http, :send_user_agent], true)
on_exit(fn ->
Pleroma.Config.put([:http, :send_user_agent], send)
end)
assert RequestBuilder.headers(%{}, []) == %{ assert RequestBuilder.headers(%{}, []) == %{
headers: [{"User-Agent", Pleroma.Application.user_agent()}] headers: [{"User-Agent", Pleroma.Application.user_agent()}]
} }

View File

@ -15,6 +15,13 @@ defmodule Pleroma.ListTest 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

@ -159,32 +159,28 @@ defmodule Pleroma.Object.FetcherTest do
end end
describe "signed fetches" do describe "signed fetches" do
clear_config([:activitypub, :sign_object_fetches])
test_with_mock "it signs fetches when configured to do so", test_with_mock "it signs fetches when configured to do so",
Pleroma.Signature, Pleroma.Signature,
[:passthrough], [:passthrough],
[] do [] do
option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
Pleroma.Config.put([:activitypub, :sign_object_fetches], true) Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert called(Pleroma.Signature.sign(:_, :_)) assert called(Pleroma.Signature.sign(:_, :_))
Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
end end
test_with_mock "it doesn't sign fetches when not configured to do so", test_with_mock "it doesn't sign fetches when not configured to do so",
Pleroma.Signature, Pleroma.Signature,
[:passthrough], [:passthrough],
[] do [] do
option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
Pleroma.Config.put([:activitypub, :sign_object_fetches], false) Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
refute called(Pleroma.Signature.sign(:_, :_)) refute called(Pleroma.Signature.sign(:_, :_))
Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
end end
end end
end end

View File

@ -9,8 +9,10 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.User alias Pleroma.User
clear_config([:instance, :public])
test "it halts if not public and no user is assigned", %{conn: conn} do test "it halts if not public and no user is assigned", %{conn: conn} do
set_public_to(false) Config.put([:instance, :public], false)
conn = conn =
conn conn
@ -21,7 +23,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
end end
test "it continues if public", %{conn: conn} do test "it continues if public", %{conn: conn} do
set_public_to(true) Config.put([:instance, :public], true)
ret_conn = ret_conn =
conn conn
@ -31,7 +33,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
end end
test "it continues if a user is assigned, even if not public", %{conn: conn} do test "it continues if a user is assigned, even if not public", %{conn: conn} do
set_public_to(false) Config.put([:instance, :public], false)
conn = conn =
conn conn
@ -43,13 +45,4 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
assert ret_conn == conn assert ret_conn == conn
end end
defp set_public_to(value) do
orig = Config.get!([:instance, :public])
Config.put([:instance, :public], value)
on_exit(fn ->
Config.put([:instance, :public], orig)
end)
end
end end

View File

@ -7,17 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
alias Pleroma.Config alias Pleroma.Config
alias Plug.Conn alias Plug.Conn
clear_config([:http_securiy, :enabled])
clear_config([:http_security, :sts])
describe "http security enabled" do describe "http security enabled" do
setup do setup do
enabled = Config.get([:http_securiy, :enabled])
Config.put([:http_security, :enabled], true) Config.put([:http_security, :enabled], true)
on_exit(fn ->
Config.put([:http_security, :enabled], enabled)
end)
:ok
end end
test "it sends CSP headers when enabled", %{conn: conn} do test "it sends CSP headers when enabled", %{conn: conn} do
@ -81,14 +76,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
end end
test "it does not send CSP headers when disabled", %{conn: conn} do test "it does not send CSP headers when disabled", %{conn: conn} do
enabled = Config.get([:http_securiy, :enabled])
Config.put([:http_security, :enabled], false) Config.put([:http_security, :enabled], false)
on_exit(fn ->
Config.put([:http_security, :enabled], enabled)
end)
conn = get(conn, "/api/v1/instance") conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "x-xss-protection") == [] assert Conn.get_resp_header(conn, "x-xss-protection") == []

View File

@ -8,14 +8,12 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do
@dir "test/tmp/instance_static" @dir "test/tmp/instance_static"
setup do setup do
static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], @dir)
File.mkdir_p!(@dir) File.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
on_exit(fn -> clear_config([:instance, :static_dir]) do
Pleroma.Config.put([:instance, :static_dir], static_dir) Pleroma.Config.put([:instance, :static_dir], @dir)
File.rm_rf(@dir)
end)
end end
test "overrides index" do test "overrides index" do

View File

@ -108,11 +108,11 @@ defmodule Pleroma.ReverseProxyTest do
end end
end end
test "max_body_size returns error if streaming body more than that option", %{conn: conn} do test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
stream_mock(3, true) stream_mock(3, true)
assert capture_log(fn -> assert capture_log(fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_size: 30) ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
end) =~ end) =~
"[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
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 @@ defmodule Pleroma.SignatureTest 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 @@ defmodule Pleroma.Factory 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 @@ defmodule Pleroma.Factory 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

@ -7,8 +7,52 @@ defmodule Pleroma.Tests.Helpers do
Helpers for use in tests. Helpers for use in tests.
""" """
defmacro clear_config(config_path) do
quote do
clear_config(unquote(config_path)) do
end
end
end
defmacro clear_config(config_path, do: yield) do
quote do
setup do
initial_setting = Pleroma.Config.get(unquote(config_path))
unquote(yield)
on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
:ok
end
end
end
defmacro clear_config_all(config_path) do
quote do
clear_config_all(unquote(config_path)) do
end
end
end
defmacro clear_config_all(config_path, do: yield) do
quote do
setup_all do
initial_setting = Pleroma.Config.get(unquote(config_path))
unquote(yield)
on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
:ok
end
end
end
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
import Pleroma.Tests.Helpers,
only: [
clear_config: 1,
clear_config: 2,
clear_config_all: 1,
clear_config_all: 2
]
def collect_ids(collection) do def collect_ids(collection) do
collection collection
|> Enum.map(& &1.id) |> Enum.map(& &1.id)
@ -30,6 +74,15 @@ defmodule Pleroma.Tests.Helpers do
|> Poison.encode!() |> Poison.encode!()
|> Poison.decode!() |> Poison.decode!()
end end
defmacro guards_config(config_path) do
quote do
initial_setting = Pleroma.Config.get(config_path)
Pleroma.Config.put(config_path, true)
on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
end
end
end end
end end
end end

View File

@ -17,9 +17,12 @@ defmodule HttpRequestMock do
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 @@ defmodule HttpRequestMock 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 @@ defmodule HttpRequestMock do
}} }}
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 @@ defmodule HttpRequestMock 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

@ -11,21 +11,20 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
Mix.shell(Mix.Shell.Process) Mix.shell(Mix.Shell.Process)
temp_file = "config/temp.exported_from_db.secret.exs" temp_file = "config/temp.exported_from_db.secret.exs"
dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
Pleroma.Config.put([:instance, :dynamic_configuration], true)
on_exit(fn -> on_exit(fn ->
Mix.shell(Mix.Shell.IO) Mix.shell(Mix.Shell.IO)
Application.delete_env(:pleroma, :first_setting) Application.delete_env(:pleroma, :first_setting)
Application.delete_env(:pleroma, :second_setting) Application.delete_env(:pleroma, :second_setting)
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
:ok = File.rm(temp_file) :ok = File.rm(temp_file)
end) end)
{:ok, temp_file: temp_file} {:ok, temp_file: temp_file}
end end
clear_config_all([:instance, :dynamic_configuration]) do
Pleroma.Config.put([:instance, :dynamic_configuration], true)
end
test "settings are migrated to db" do test "settings are migrated to db" do
assert Repo.all(Config) == [] assert Repo.all(Config) == []

View File

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.DatabaseTest do defmodule Mix.Tasks.Pleroma.DatabaseTest do
alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -22,6 +23,52 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
:ok :ok
end end
describe "running remove_embedded_objects" do
test "it replaces objects with references" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
new_data = Map.put(activity.data, "object", activity.object.data)
{:ok, activity} =
activity
|> Activity.change(%{data: new_data})
|> Repo.update()
assert is_map(activity.data["object"])
Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"])
activity = Activity.get_by_id_with_object(activity.id)
assert is_binary(activity.data["object"])
end
end
describe "prune_objects" do
test "it prunes old objects from the database" do
insert(:note)
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
%{id: id} =
:note
|> insert()
|> Ecto.Changeset.change(%{inserted_at: date})
|> Repo.update!()
assert length(Repo.all(Object)) == 2
Mix.Tasks.Pleroma.Database.run(["prune_objects"])
assert length(Repo.all(Object)) == 1
refute Object.get_by_id(id)
end
end
describe "running update_users_following_followers_counts" do describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do test "following and followers count are updated" do
[user, user2] = insert_pair(:user) [user, user2] = insert_pair(:user)

View File

@ -44,7 +44,7 @@ defmodule Mix.Tasks.Pleroma.DigestTest do
assert_email_sent( assert_email_sent(
to: {user2.name, user2.email}, to: {user2.name, user2.email},
html_body: ~r/new mentions:/i html_body: ~r/here is what you've missed!/i
) )
end end
end end

View File

@ -50,7 +50,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest 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,30 @@ defmodule Mix.Tasks.Pleroma.RelayTest 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
describe "mix pleroma.relay list" do
test "Prints relay subscription list" do
:ok = Mix.Tasks.Pleroma.Relay.run(["list"])
refute_receive {:mix_shell, :info, _}
Pleroma.Web.ActivityPub.Relay.get_actor()
|> Ecto.Changeset.change(
following: [
"http://test-app.com/user/test1",
"http://test-app.com/user/test1",
"http://test-app-42.com/user/test1"
]
)
|> Pleroma.User.update_and_set_cache()
:ok = Mix.Tasks.Pleroma.Relay.run(["list"])
assert_receive {:mix_shell, :info, ["test-app.com"]}
assert_receive {:mix_shell, :info, ["test-app-42.com"]}
end end
end end
end end

View File

@ -4,17 +4,17 @@
defmodule Mix.Tasks.Pleroma.RobotsTxtTest do defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
use ExUnit.Case use ExUnit.Case
use Pleroma.Tests.Helpers
alias Mix.Tasks.Pleroma.RobotsTxt alias Mix.Tasks.Pleroma.RobotsTxt
clear_config([:instance, :static_dir])
test "creates new dir" do test "creates new dir" do
path = "test/fixtures/new_dir/" path = "test/fixtures/new_dir/"
file_path = path <> "robots.txt" file_path = path <> "robots.txt"
static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], path) Pleroma.Config.put([:instance, :static_dir], path)
on_exit(fn -> on_exit(fn ->
Pleroma.Config.put([:instance, :static_dir], static_dir)
{:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path)
end) end)
@ -29,11 +29,9 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
test "to existance folder" do test "to existance folder" do
path = "test/fixtures/" path = "test/fixtures/"
file_path = path <> "robots.txt" file_path = path <> "robots.txt"
static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], path) Pleroma.Config.put([:instance, :static_dir], path)
on_exit(fn -> on_exit(fn ->
Pleroma.Config.put([:instance, :static_dir], static_dir)
:ok = File.rm(file_path) :ok = File.rm(file_path)
end) end)

View File

@ -9,12 +9,6 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
alias Pleroma.Upload alias Pleroma.Upload
setup do setup do
custom_filename = Config.get([Upload.Filter.AnonymizeFilename, :text])
on_exit(fn ->
Config.put([Upload.Filter.AnonymizeFilename, :text], custom_filename)
end)
upload_file = %Upload{ upload_file = %Upload{
name: "an… image.jpg", name: "an… image.jpg",
content_type: "image/jpg", content_type: "image/jpg",
@ -24,6 +18,8 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
%{upload_file: upload_file} %{upload_file: upload_file}
end end
clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
{:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)

View File

@ -10,13 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.Upload.Filter alias Pleroma.Upload.Filter
setup do clear_config([Filter.Mogrify, :args])
filter = Config.get([Filter.Mogrify, :args])
on_exit(fn ->
Config.put([Filter.Mogrify, :args], filter)
end)
end
test "apply mogrify filter" do test "apply mogrify filter" do
Config.put([Filter.Mogrify, :args], [{"tint", "40"}]) Config.put([Filter.Mogrify, :args], [{"tint", "40"}])

View File

@ -8,13 +8,7 @@ defmodule Pleroma.Upload.FilterTest do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Upload.Filter alias Pleroma.Upload.Filter
setup do clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text])
on_exit(fn ->
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename)
end)
end
test "applies filters" do test "applies filters" do
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")

View File

@ -250,12 +250,8 @@ defmodule Pleroma.UploadTest do
end end
describe "Setting a custom base_url for uploaded media" do describe "Setting a custom base_url for uploaded media" do
setup do clear_config([Pleroma.Upload, :base_url]) do
Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social") Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social")
on_exit(fn ->
Pleroma.Config.put([Pleroma.Upload, :base_url], nil)
end)
end end
test "returns a media url with configured base_url" do test "returns a media url with configured base_url" do

View File

@ -11,19 +11,11 @@ defmodule Pleroma.Uploaders.S3Test do
import Mock import Mock
import ExUnit.CaptureLog import ExUnit.CaptureLog
setup do clear_config([Pleroma.Uploaders.S3]) do
config = Config.get([Pleroma.Uploaders.S3])
Config.put([Pleroma.Uploaders.S3], Config.put([Pleroma.Uploaders.S3],
bucket: "test_bucket", bucket: "test_bucket",
public_endpoint: "https://s3.amazonaws.com" public_endpoint: "https://s3.amazonaws.com"
) )
on_exit(fn ->
Config.put([Pleroma.Uploaders.S3], config)
end)
:ok
end end
describe "get_file/1" do describe "get_file/1" do

View File

@ -21,6 +21,8 @@ defmodule Pleroma.UserTest do
:ok :ok
end end
clear_config([:instance, :account_activation_required])
describe "when tags are nil" do describe "when tags are nil" do
test "tagging a user" do test "tagging a user" do
user = insert(:user, %{tags: nil}) user = insert(:user, %{tags: nil})
@ -203,24 +205,64 @@ defmodule Pleroma.UserTest do
# assert websub # assert websub
# end # end
test "unfollow takes a user and another user" do describe "unfollow/2" do
followed = insert(:user) setup do
user = insert(:user, %{following: [User.ap_followers(followed)]}) setting = Pleroma.Config.get([:instance, :external_user_synchronization])
{:ok, user, _activity} = User.unfollow(user, followed) on_exit(fn ->
Pleroma.Config.put([:instance, :external_user_synchronization], setting)
end)
user = User.get_cached_by_id(user.id) :ok
end
assert user.following == [] test "unfollow with syncronizes external user" do
end Pleroma.Config.put([:instance, :external_user_synchronization], true)
test "unfollow doesn't unfollow yourself" do followed =
user = insert(:user) insert(:user,
nickname: "fuser1",
follower_address: "http://localhost:4001/users/fuser1/followers",
following_address: "http://localhost:4001/users/fuser1/following",
ap_id: "http://localhost:4001/users/fuser1"
)
{:error, _} = User.unfollow(user, user) user =
insert(:user, %{
local: false,
nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following",
following: [User.ap_followers(followed)]
})
user = User.get_cached_by_id(user.id) {:ok, user, _activity} = User.unfollow(user, followed)
assert user.following == [user.ap_id]
user = User.get_cached_by_id(user.id)
assert user.following == []
end
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert user.following == []
end
test "unfollow doesn't unfollow yourself" do
user = insert(:user)
{:error, _} = User.unfollow(user, user)
user = User.get_cached_by_id(user.id)
assert user.following == [user.ap_id]
end
end end
test "test if a user is following another user" do test "test if a user is following another user" do
@ -247,6 +289,9 @@ defmodule Pleroma.UserTest do
password_confirmation: "test", password_confirmation: "test",
email: "email@example.com" email: "email@example.com"
} }
clear_config([:instance, :autofollowed_nicknames])
clear_config([:instance, :welcome_message])
clear_config([:instance, :welcome_user_nickname])
test "it autofollows accounts that are set for it" do test "it autofollows accounts that are set for it" do
user = insert(:user) user = insert(:user)
@ -263,8 +308,6 @@ defmodule Pleroma.UserTest do
assert User.following?(registered_user, user) assert User.following?(registered_user, user)
refute User.following?(registered_user, remote_user) refute User.following?(registered_user, remote_user)
Pleroma.Config.put([:instance, :autofollowed_nicknames], [])
end end
test "it sends a welcome message if it is set" do test "it sends a welcome message if it is set" do
@ -280,9 +323,6 @@ defmodule Pleroma.UserTest do
assert registered_user.ap_id in activity.recipients assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "cool site" assert Object.normalize(activity).data["content"] =~ "cool site"
assert activity.actor == welcome_user.ap_id assert activity.actor == welcome_user.ap_id
Pleroma.Config.put([:instance, :welcome_user_nickname], nil)
Pleroma.Config.put([:instance, :welcome_message], nil)
end end
test "it requires an email, name, nickname and password, bio is optional" do test "it requires an email, name, nickname and password, bio is optional" do
@ -348,15 +388,8 @@ defmodule Pleroma.UserTest do
email: "email@example.com" email: "email@example.com"
} }
setup do clear_config([:instance, :account_activation_required]) do
setting = Pleroma.Config.get([:instance, :account_activation_required]) Pleroma.Config.put([:instance, :account_activation_required], true)
unless setting do
Pleroma.Config.put([:instance, :account_activation_required], true)
on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
end
:ok
end end
test "it creates unconfirmed user" do test "it creates unconfirmed user" do
@ -508,6 +541,9 @@ defmodule Pleroma.UserTest do
avatar: %{some: "avatar"} avatar: %{some: "avatar"}
} }
clear_config([:instance, :user_bio_length])
clear_config([:instance, :user_name_length])
test "it confirms validity" do test "it confirms validity" do
cs = User.remote_user_creation(@valid_remote) cs = User.remote_user_creation(@valid_remote)
assert cs.valid? assert cs.valid?
@ -1003,6 +1039,8 @@ defmodule Pleroma.UserTest do
[user: user] [user: user]
end end
clear_config([:instance, :federating])
test ".delete_user_activities deletes all create activities", %{user: user} do test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
@ -1012,6 +1050,13 @@ defmodule Pleroma.UserTest do
refute Activity.get_by_id(activity.id) refute Activity.get_by_id(activity.id)
end end
test "it deletes deactivated user" do
{:ok, user} = insert(:user, info: %{deactivated: true}) |> User.set_cache()
assert {:ok, _} = User.delete(user)
refute User.get_by_id(user.id)
end
test "it deletes a user, all follow relationships and all activities", %{user: user} do test "it deletes a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user) follower = insert(:user)
{:ok, follower} = User.follow(follower, user) {:ok, follower} = User.follow(follower, user)
@ -1053,9 +1098,7 @@ defmodule Pleroma.UserTest do
Pleroma.Web.ActivityPub.Publisher, Pleroma.Web.ActivityPub.Publisher,
[:passthrough], [:passthrough],
[] do [] do
config_path = [:instance, :federating] Pleroma.Config.put([:instance, :federating], true)
initial_setting = Pleroma.Config.get(config_path)
Pleroma.Config.put(config_path, true)
{:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") {:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
{:ok, _} = User.follow(follower, user) {:ok, _} = User.follow(follower, user)
@ -1067,8 +1110,6 @@ defmodule Pleroma.UserTest do
inbox: "http://mastodon.example.org/inbox" inbox: "http://mastodon.example.org/inbox"
}) })
) )
Pleroma.Config.put(config_path, initial_setting)
end end
end end
@ -1134,8 +1175,6 @@ defmodule Pleroma.UserTest do
refute User.auth_active?(local_user) refute User.auth_active?(local_user)
assert User.auth_active?(confirmed_user) assert User.auth_active?(confirmed_user)
assert User.auth_active?(remote_user) assert User.auth_active?(remote_user)
Pleroma.Config.put([:instance, :account_activation_required], false)
end end
describe "superuser?/1" do describe "superuser?/1" do
@ -1180,8 +1219,6 @@ defmodule Pleroma.UserTest do
other_user = insert(:user, local: true) other_user = insert(:user, local: true)
refute User.visible_for?(user, other_user) refute User.visible_for?(user, other_user)
Pleroma.Config.put([:instance, :account_activation_required], false)
end end
test "returns true when the account is unauthenticated and auth is not required" do test "returns true when the account is unauthenticated and auth is not required" do
@ -1198,8 +1235,6 @@ defmodule Pleroma.UserTest do
other_user = insert(:user, local: true, info: %{is_admin: true}) other_user = insert(:user, local: true, info: %{is_admin: true})
assert User.visible_for?(user, other_user) assert User.visible_for?(user, other_user)
Pleroma.Config.put([:instance, :account_activation_required], false)
end end
end end
@ -1218,18 +1253,18 @@ defmodule Pleroma.UserTest 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
@ -1512,10 +1547,7 @@ defmodule Pleroma.UserTest do
end end
describe "following/followers synchronization" do describe "following/followers synchronization" do
setup do clear_config([:instance, :external_user_synchronization])
sync = Pleroma.Config.get([:instance, :external_user_synchronization])
on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end)
end
test "updates the counters normally on following/getting a follow when disabled" do test "updates the counters normally on following/getting a follow when disabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false) Pleroma.Config.put([:instance, :external_user_synchronization], false)

View File

@ -10,23 +10,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Object alias Pleroma.Object
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
setup_all do setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
config_path = [:instance, :federating]
initial_setting = Pleroma.Config.get(config_path)
Pleroma.Config.put(config_path, true)
on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
:ok :ok
end end
clear_config_all([:instance, :federating],
do: Pleroma.Config.put([:instance, :federating], true)
)
describe "/relay" do describe "/relay" do
clear_config([:instance, :allow_relay])
test "with the relay active, it returns the relay user", %{conn: conn} do test "with the relay active, it returns the relay user", %{conn: conn} do
res = res =
conn conn
@ -43,8 +43,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> get(activity_pub_path(conn, :relay)) |> get(activity_pub_path(conn, :relay))
|> json_response(404) |> json_response(404)
|> assert |> assert
Pleroma.Config.put([:instance, :allow_relay], true)
end end
end end
@ -596,6 +594,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest 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)
@ -555,7 +557,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
note_two = insert(:note, data: %{"context" => "suya.."}) note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two) activity_two = insert(:note_activity, note: note_two)
{:ok, activity_two} = CommonAPI.add_mute(user, activity_two) {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_two, _activity_one] = assert [_activity_two, _activity_one] =
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true}) ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
@ -676,6 +678,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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, 5))
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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, 5))
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

@ -1,5 +1,6 @@
defmodule Pleroma.Web.ActivityPub.MRFTest do defmodule Pleroma.Web.ActivityPub.MRFTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
use Pleroma.Tests.Helpers
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
test "subdomains_regex/1" do test "subdomains_regex/1" do
@ -59,6 +60,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
end end
describe "describe/0" do describe "describe/0" do
clear_config([:instance, :rewrite_policy])
test "it works as expected with noop policy" do test "it works as expected with noop policy" do
expected = %{ expected = %{
mrf_policies: ["NoOpPolicy"], mrf_policies: ["NoOpPolicy"],
@ -69,7 +72,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
end end
test "it works as expected with mock policy" do test "it works as expected with mock policy" do
config = Pleroma.Config.get([:instance, :rewrite_policy])
Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock]) Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock])
expected = %{ expected = %{
@ -79,8 +81,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
} }
{:ok, ^expected} = MRF.describe() {:ok, ^expected} = MRF.describe()
Pleroma.Config.put([:instance, :rewrite_policy], config)
end end
end end
end end

View File

@ -8,12 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do
alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic
setup do clear_config([:mrf_rejectnonpublic])
policy = Pleroma.Config.get([:mrf_rejectnonpublic])
on_exit(fn -> Pleroma.Config.put([:mrf_rejectnonpublic], policy) end)
:ok
end
describe "public message" do describe "public message" do
test "it's allowed when address is public" do test "it's allowed when address is public" do

View File

@ -8,9 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF.SimplePolicy alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
setup do clear_config([:mrf_simple]) do
orig = Config.get!(:mrf_simple)
Config.put(:mrf_simple, Config.put(:mrf_simple,
media_removal: [], media_removal: [],
media_nsfw: [], media_nsfw: [],
@ -21,10 +19,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
avatar_removal: [], avatar_removal: [],
banner_removal: [] banner_removal: []
) )
on_exit(fn ->
Config.put(:mrf_simple, orig)
end)
end end
describe "when :media_removal" do describe "when :media_removal" do

View File

@ -7,12 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
setup do clear_config([:mrf_user_allowlist, :localhost])
policy = Pleroma.Config.get([:mrf_user_allowlist]) || []
on_exit(fn -> Pleroma.Config.put([:mrf_user_allowlist], policy) end)
:ok
end
test "pass filter if allow list is empty" do test "pass filter if allow list is empty" do
actor = insert(:user) actor = insert(:user)

View File

@ -8,8 +8,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy
describe "accept" do describe "accept" do
clear_config([:mrf_vocabulary, :accept])
test "it accepts based on parent activity type" do test "it accepts based on parent activity type" do
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"])
message = %{ message = %{
@ -18,12 +19,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:ok, ^message} = VocabularyPolicy.filter(message) {:ok, ^message} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
end end
test "it accepts based on child object type" do test "it accepts based on child object type" do
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
message = %{ message = %{
@ -35,12 +33,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:ok, ^message} = VocabularyPolicy.filter(message) {:ok, ^message} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
end end
test "it does not accept disallowed child objects" do test "it does not accept disallowed child objects" do
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
message = %{ message = %{
@ -52,12 +47,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:reject, nil} = VocabularyPolicy.filter(message) {:reject, nil} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
end end
test "it does not accept disallowed parent types" do test "it does not accept disallowed parent types" do
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"])
message = %{ message = %{
@ -69,14 +61,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:reject, nil} = VocabularyPolicy.filter(message) {:reject, nil} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
end end
end end
describe "reject" do describe "reject" do
clear_config([:mrf_vocabulary, :reject])
test "it rejects based on parent activity type" do test "it rejects based on parent activity type" do
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
message = %{ message = %{
@ -85,12 +76,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:reject, nil} = VocabularyPolicy.filter(message) {:reject, nil} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
end end
test "it rejects based on child object type" do test "it rejects based on child object type" do
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"])
message = %{ message = %{
@ -102,12 +90,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:reject, nil} = VocabularyPolicy.filter(message) {:reject, nil} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
end end
test "it passes through objects that aren't disallowed" do test "it passes through objects that aren't disallowed" do
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
message = %{ message = %{
@ -116,8 +101,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
} }
{:ok, ^message} = VocabularyPolicy.filter(message) {:ok, ^message} = VocabularyPolicy.filter(message)
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
end end
end end
end end

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 @@ defmodule Pleroma.Web.ActivityPub.RelayTest 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 @@ defmodule Pleroma.Web.ActivityPub.RelayTest 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, 5))
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, 5))
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

@ -24,6 +24,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
:ok :ok
end end
clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity) activity = insert(:note_activity)
@ -509,6 +511,68 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert user.bio == "<p>Some bio</p>" assert user.bio == "<p>Some bio</p>"
end end
test "it works with custom profile fields" do
{:ok, activity} =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Poison.decode!()
|> Transmogrifier.handle_incoming()
user = User.get_cached_by_ap_id(activity.actor)
assert User.Info.fields(user.info) == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "foo1", "value" => "bar1"}
]
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
{:ok, _update_activity} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.Info.fields(user.info) == [
%{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"}
]
Pleroma.Config.put([:instance, :max_remote_account_fields], 2)
update_data =
put_in(update_data, ["object", "attachment"], [
%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"},
%{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"},
%{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"}
])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.Info.fields(user.info) == [
%{"name" => "foo", "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
test "it works for incoming update activities which lock the account" do test "it works for incoming update activities which lock the account" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()

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 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest 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 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest 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

@ -22,6 +22,21 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY")
end end
test "Renders profile fields" do
fields = [
%{"name" => "foo", "value" => "bar"}
]
{:ok, user} =
insert(:user)
|> User.upgrade_changeset(%{info: %{fields: fields}})
|> User.update_and_set_cache()
assert %{
"attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}]
} = UserView.render("user.json", %{user: user})
end
test "Does not add an avatar image if the user hasn't set one" do test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user) user = insert(:user)
{:ok, user} = User.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)

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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}&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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@ -294,20 +483,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
describe "POST /api/pleroma/admin/email_invite, with valid config" do describe "POST /api/pleroma/admin/email_invite, with valid config" do
setup do setup do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
[user: insert(:user, info: %{is_admin: true})] [user: insert(:user, info: %{is_admin: true})]
end end
clear_config([:instance, :registrations_open]) do
Pleroma.Config.put([:instance, :registrations_open], false)
end
clear_config([:instance, :invites_enabled]) do
Pleroma.Config.put([:instance, :invites_enabled], true)
end
test "sends invitation and returns 204", %{conn: conn, user: user} do test "sends invitation and returns 204", %{conn: conn, user: user} do
recipient_email = "foo@bar.com" recipient_email = "foo@bar.com"
recipient_name = "J. D." recipient_name = "J. D."
@ -360,18 +546,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
[user: insert(:user, info: %{is_admin: true})] [user: insert(:user, info: %{is_admin: true})]
end end
clear_config([:instance, :registrations_open])
clear_config([:instance, :invites_enabled])
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], false) Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], false) Pleroma.Config.put([:instance, :invites_enabled], false)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
conn = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
@ -381,17 +562,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], true) Pleroma.Config.put([:instance, :registrations_open], true)
Pleroma.Config.put([:instance, :invites_enabled], true) Pleroma.Config.put([:instance, :invites_enabled], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
conn = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
@ -884,6 +1057,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@ -1069,25 +1247,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@ -1218,14 +1406,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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)
@ -1248,6 +1437,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@ -1271,10 +1467,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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"})
@ -1282,6 +1478,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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"})
@ -1290,7 +1491,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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"})
@ -1298,6 +1499,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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"})
@ -1327,15 +1533,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@ -1402,17 +1613,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
:ok = File.rm(temp_file) :ok = File.rm(temp_file)
end) end)
dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
Pleroma.Config.put([:instance, :dynamic_configuration], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
end)
%{conn: assign(conn, :user, admin)} %{conn: assign(conn, :user, admin)}
end end
clear_config([:instance, :dynamic_configuration]) do
Pleroma.Config.put([:instance, :dynamic_configuration], true)
end
test "create new config setting in db", %{conn: conn} do test "create new config setting in db", %{conn: conn} do
conn = conn =
post(conn, "/api/pleroma/admin/config", %{ post(conn, "/api/pleroma/admin/config", %{
@ -1961,17 +2168,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
:ok = File.rm(temp_file) :ok = File.rm(temp_file)
end) end)
dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
Pleroma.Config.put([:instance, :dynamic_configuration], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
end)
%{conn: assign(conn, :user, admin), admin: admin} %{conn: assign(conn, :user, admin), admin: admin}
end end
clear_config([:instance, :dynamic_configuration]) do
Pleroma.Config.put([:instance, :dynamic_configuration], true)
end
test "transfer settings to DB and to file", %{conn: conn, admin: admin} do test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
@ -2044,6 +2247,108 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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

Some files were not shown because too many files have changed in this diff Show More