Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
This commit is contained in:
commit
cc28184854
120 changed files with 4113 additions and 813 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -18,10 +18,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- NodeInfo: Return `mailerEnabled` in `metadata`
|
||||
- Mastodon API: Unsubscribe followers when they unfollow a user
|
||||
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
|
||||
- Improve digest email template
|
||||
|
||||
### Fixed
|
||||
- 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.
|
||||
- Favorites timeline doing database-intensive queries
|
||||
- Metadata rendering errors resulting in the entire page being inaccessible
|
||||
- `federation_incoming_replies_max_depth` option being ignored in certain cases
|
||||
- Federation/MediaProxy not working with instances that have wrong certificate order
|
||||
|
@ -43,9 +45,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.
|
||||
- Report email not being sent to admins when the reporter is a remote user
|
||||
- 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
|
||||
- ActivityPub: Deactivated user deletion
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
|
||||
|
@ -66,6 +75,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: 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: Improve support for the user profile custom fields
|
||||
- Admin API: Return users' tags when querying reports
|
||||
- Admin API: Return avatar and display name when querying users
|
||||
- Admin API: Allow querying user by ID
|
||||
|
@ -84,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Relays: Added a task to list relay subscriptions.
|
||||
- Mix Tasks: `mix pleroma.database fix_likes_collections`
|
||||
- Federation: Remove `likes` from objects.
|
||||
- Admin API: Added moderation log
|
||||
|
||||
### Changed
|
||||
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
||||
|
@ -185,6 +196,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.
|
||||
- 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: `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
|
||||
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
|
||||
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
|
||||
|
|
|
@ -255,6 +255,10 @@
|
|||
dynamic_configuration: false,
|
||||
user_bio_length: 5000,
|
||||
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
|
||||
|
||||
config :pleroma, :markup,
|
||||
|
@ -452,6 +456,7 @@
|
|||
max_retries: 5
|
||||
|
||||
config :pleroma_job_queue, :queues,
|
||||
activity_expiration: 10,
|
||||
federator_incoming: 50,
|
||||
federator_outgoing: 50,
|
||||
web_push: 50,
|
||||
|
@ -512,6 +517,17 @@
|
|||
|
||||
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 :pleroma, Pleroma.ScheduledActivity,
|
||||
|
@ -540,15 +556,9 @@
|
|||
config :http_signatures,
|
||||
adapter: Pleroma.Signature
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
search: [{1000, 10}, {1000, 30}],
|
||||
app_account_creation: {1_800_000, 25},
|
||||
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}
|
||||
config :pleroma, :rate_limit, nil
|
||||
|
||||
config :pleroma, Pleroma.ActivityExpiration, enabled: true
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
|
|
|
@ -71,7 +71,8 @@
|
|||
config :pleroma, :rate_limit,
|
||||
search: [{1000, 30}, {1000, 30}],
|
||||
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"
|
||||
|
||||
|
@ -85,11 +86,10 @@
|
|||
|
||||
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
|
||||
|
||||
try do
|
||||
if File.exists?("./config/test.secret.exs") do
|
||||
import_config "test.secret.exs"
|
||||
rescue
|
||||
_ ->
|
||||
IO.puts(
|
||||
"You may want to create test.secret.exs to declare custom database connection parameters."
|
||||
)
|
||||
else
|
||||
IO.puts(
|
||||
"You may want to create test.secret.exs to declare custom database connection parameters."
|
||||
)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
- `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`
|
||||
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
|
||||
|
||||
## Attachments
|
||||
|
||||
|
@ -59,12 +60,19 @@ Has these additional fields under the `pleroma` object:
|
|||
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
|
||||
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
|
||||
|
||||
## Conversations
|
||||
|
||||
Has an additional field under the `pleroma` object:
|
||||
|
||||
- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation.
|
||||
|
||||
## Account Search
|
||||
|
||||
Behavior has changed:
|
||||
|
||||
- `/api/v1/accounts/search`: Does not require authentication
|
||||
|
||||
|
||||
## Notifications
|
||||
|
||||
Has these additional fields under the `pleroma` object:
|
||||
|
@ -79,6 +87,8 @@ 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.
|
||||
- `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`.
|
||||
- `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`.
|
||||
|
||||
## PATCH `/api/v1/update_credentials`
|
||||
|
||||
|
|
|
@ -319,3 +319,38 @@ See [Admin-API](Admin-API.md)
|
|||
"healthy": true # Instance state
|
||||
}
|
||||
```
|
||||
|
||||
# Pleroma Conversations
|
||||
|
||||
Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
|
||||
|
||||
1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user.
|
||||
2. Pleroma Conversations statuses can be requested by Conversation id.
|
||||
3. Pleroma Conversations can be replied to.
|
||||
|
||||
Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation.
|
||||
|
||||
The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
|
||||
|
||||
|
||||
## `GET /api/v1/pleroma/conversations/:id/statuses`
|
||||
### Timeline for a given conversation
|
||||
* Method `GET`
|
||||
* Authentication: required
|
||||
* Params: Like other timelines
|
||||
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
|
||||
|
||||
## `GET /api/v1/pleroma/conversations/:id`
|
||||
### The conversation with the given ID.
|
||||
* Method `GET`
|
||||
* Authentication: required
|
||||
* Params: None
|
||||
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
|
||||
|
||||
## `PATCH /api/v1/pleroma/conversations/:id`
|
||||
### Update a conversation. Used to change the set of recipients.
|
||||
* Method `PATCH`
|
||||
* Authentication: required
|
||||
* Params:
|
||||
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
|
||||
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
|
||||
|
|
|
@ -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.
|
||||
* `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.
|
||||
* `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.
|
||||
|
||||
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`.
|
||||
* `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.
|
||||
* `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.
|
||||
|
||||
|
||||
|
@ -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`)
|
||||
* `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.PleromaAuthenticator`: default database authenticator
|
||||
|
@ -555,6 +563,11 @@ Email notifications settings.
|
|||
- interval: Minimum interval between digest emails to one user
|
||||
- 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 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
|
||||
|
||||
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:
|
||||
|
||||
* The first element: `scale` (Integer). The time scale in milliseconds.
|
||||
|
|
|
@ -27,7 +27,15 @@ def run(["test", nickname | opts]) do
|
|||
|
||||
patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at}
|
||||
|
||||
_user = Pleroma.DigestEmailWorker.perform(patched_user)
|
||||
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})")
|
||||
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
|
||||
{: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
|
||||
|
|
|
@ -53,13 +53,11 @@ def run(["unfollow", target]) do
|
|||
def run(["list"]) do
|
||||
start_pleroma()
|
||||
|
||||
with %User{} = user <- Relay.get_actor() do
|
||||
user.following
|
||||
|> Enum.each(fn entry ->
|
||||
URI.parse(entry)
|
||||
|> Map.get(:host)
|
||||
|> shell_info()
|
||||
end)
|
||||
with %User{following: following} = _user <- Relay.get_actor() do
|
||||
following
|
||||
|> Enum.map(fn entry -> URI.parse(entry).host end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(&shell_info(&1))
|
||||
else
|
||||
e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Activity do
|
|||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
@ -59,6 +60,8 @@ defmodule Pleroma.Activity do
|
|||
# typical case.
|
||||
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
|
||||
|
||||
has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -96,6 +99,7 @@ def with_set_thread_muted_field(query, %User{} = user) do
|
|||
from([a] in query,
|
||||
left_join: tm in ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
|
||||
as: :thread_mute,
|
||||
select: %Activity{a | thread_muted?: not is_nil(tm.id)}
|
||||
)
|
||||
end
|
||||
|
|
68
lib/pleroma/activity_expiration.ex
Normal file
68
lib/pleroma/activity_expiration.ex
Normal 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
|
62
lib/pleroma/activity_expiration_worker.ex
Normal file
62
lib/pleroma/activity_expiration_worker.ex
Normal 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
|
|
@ -35,7 +35,8 @@ def start(_type, _args) do
|
|||
Pleroma.Emoji,
|
||||
Pleroma.Captcha,
|
||||
Pleroma.FlakeId,
|
||||
Pleroma.ScheduledActivityWorker
|
||||
Pleroma.ScheduledActivityWorker,
|
||||
Pleroma.ActivityExpirationWorker
|
||||
] ++
|
||||
cachex_children() ++
|
||||
hackney_pool_children() ++
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Conversation do
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Conversation.Participation.RecipientShip
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
use Ecto.Schema
|
||||
|
@ -39,6 +40,15 @@ def get_for_ap_id(ap_id) do
|
|||
Repo.get_by(__MODULE__, ap_id: ap_id)
|
||||
end
|
||||
|
||||
def maybe_create_recipientships(participation, activity) do
|
||||
participation = Repo.preload(participation, :recipients)
|
||||
|
||||
if participation.recipients |> Enum.empty?() do
|
||||
recipients = User.get_all_by_ap_id(activity.recipients)
|
||||
RecipientShip.create(recipients, participation)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This will
|
||||
1. Create a conversation if there isn't one already
|
||||
|
@ -60,6 +70,7 @@ def create_or_bump_for(activity, opts \\ []) do
|
|||
{:ok, participation} =
|
||||
Participation.create_for_user_and_conversation(user, conversation, opts)
|
||||
|
||||
maybe_create_recipientships(participation, activity)
|
||||
participation
|
||||
end)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Conversation.Participation do
|
||||
use Ecto.Schema
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation.RecipientShip
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
@ -17,6 +18,9 @@ defmodule Pleroma.Conversation.Participation do
|
|||
field(:read, :boolean, default: false)
|
||||
field(:last_activity_id, Pleroma.FlakeId, virtual: true)
|
||||
|
||||
has_many(:recipient_ships, RecipientShip)
|
||||
has_many(:recipients, through: [:recipient_ships, :user])
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -65,6 +69,14 @@ def for_user(user, params \\ %{}) do
|
|||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
def for_user_and_conversation(user, conversation) do
|
||||
from(p in __MODULE__,
|
||||
where: p.user_id == ^user.id,
|
||||
where: p.conversation_id == ^conversation.id
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def for_user_with_last_activity_id(user, params \\ %{}) do
|
||||
for_user(user, params)
|
||||
|> Enum.map(fn participation ->
|
||||
|
@ -81,4 +93,46 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
|
|||
end)
|
||||
|> Enum.filter(& &1.last_activity_id)
|
||||
end
|
||||
|
||||
def get(_, _ \\ [])
|
||||
def get(nil, _), do: nil
|
||||
|
||||
def get(id, params) do
|
||||
query =
|
||||
if preload = params[:preload] do
|
||||
from(p in __MODULE__,
|
||||
preload: ^preload
|
||||
)
|
||||
else
|
||||
__MODULE__
|
||||
end
|
||||
|
||||
Repo.get(query, id)
|
||||
end
|
||||
|
||||
def set_recipients(participation, user_ids) do
|
||||
user_ids =
|
||||
[participation.user_id | user_ids]
|
||||
|> Enum.uniq()
|
||||
|
||||
Repo.transaction(fn ->
|
||||
query =
|
||||
from(r in RecipientShip,
|
||||
where: r.participation_id == ^participation.id
|
||||
)
|
||||
|
||||
Repo.delete_all(query)
|
||||
|
||||
users =
|
||||
from(u in User,
|
||||
where: u.id in ^user_ids
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
RecipientShip.create(users, participation)
|
||||
:ok
|
||||
end)
|
||||
|
||||
{:ok, Repo.preload(participation, :recipients, force: true)}
|
||||
end
|
||||
end
|
||||
|
|
34
lib/pleroma/conversation/participation_recipient_ship.ex
Normal file
34
lib/pleroma/conversation/participation_recipient_ship.ex
Normal 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.Conversation.Participation.RecipientShip do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "conversation_participation_recipient_ships" do
|
||||
belongs_to(:user, User, type: Pleroma.FlakeId)
|
||||
belongs_to(:participation, Participation)
|
||||
end
|
||||
|
||||
def creation_cng(struct, params) do
|
||||
struct
|
||||
|> cast(params, [:user_id, :participation_id])
|
||||
|> validate_required([:user_id, :participation_id])
|
||||
end
|
||||
|
||||
def create(%User{} = user, participation), do: create([user], participation)
|
||||
|
||||
def create(users, participation) do
|
||||
Enum.each(users, fn user ->
|
||||
%__MODULE__{}
|
||||
|> creation_cng(%{user_id: user.id, participation_id: participation.id})
|
||||
|> Repo.insert!()
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -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
|
||||
import Ecto.Query
|
||||
|
||||
|
|
|
@ -7,21 +7,21 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
|
||||
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.Router
|
||||
|
||||
defp instance_config, do: Pleroma.Config.get(:instance)
|
||||
|
||||
defp instance_name, do: instance_config()[:name]
|
||||
defp instance_name, do: Config.get([:instance, :name])
|
||||
|
||||
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}
|
||||
end
|
||||
|
||||
defp recipient(email, nil), do: 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
|
||||
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
|
||||
|
@ -93,67 +93,86 @@ def account_confirmation_email(user) do
|
|||
Includes Mentions and New Followers data
|
||||
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
|
||||
new_notifications =
|
||||
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)
|
||||
}
|
||||
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
|
||||
|
||||
%{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 ->
|
||||
new_follower = %{
|
||||
data: notification,
|
||||
object: Pleroma.Object.normalize(activity),
|
||||
from: Pleroma.User.get_by_ap_id(actor)
|
||||
}
|
||||
|
||||
%{acc | followers: [new_follower | acc.followers]}
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
%{
|
||||
data: notification,
|
||||
object: object,
|
||||
from: User.get_by_ap_id(notification.activity.actor)
|
||||
}
|
||||
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 = %{
|
||||
instance: instance_name(),
|
||||
user: user,
|
||||
mentions: mentions,
|
||||
followers: new_notifications.followers,
|
||||
unsubscribe_link: unsubscribe_url(user, "digest")
|
||||
followers: followers,
|
||||
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()
|
||||
|> to(recipient(user))
|
||||
|> from(sender())
|
||||
|> subject("Your digest from #{instance_name()}")
|
||||
|> put_layout(false)
|
||||
|> render_body("digest.html", html_data)
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline))
|
||||
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 """
|
||||
Generate unsubscribe link for given user and notifications type.
|
||||
The link contains JWT token with the data, and subscription can be modified without
|
||||
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
|
||||
token =
|
||||
%{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false}
|
||||
|> Pleroma.JWT.generate_and_sign!()
|
||||
|> Base.encode64()
|
||||
|
||||
Router.Helpers.subscription_url(Pleroma.Web.Endpoint, :unsubscribe, token)
|
||||
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -282,3 +282,31 @@ def scrub({tag, attributes, children}), do: {tag, attributes, children}
|
|||
def scrub({_tag, children}), do: children
|
||||
def scrub(text), do: text
|
||||
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
|
||||
|
|
433
lib/pleroma/moderation_log.ex
Normal file
433
lib/pleroma/moderation_log.ex
Normal 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
|
|
@ -117,9 +117,7 @@ defp maybe_date_fetch(headers, date) do
|
|||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||
Logger.info("Fetching object #{id} via AP")
|
||||
|
||||
date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
headers =
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|
|
|
@ -109,7 +109,11 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
|
|||
end
|
||||
|
||||
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)
|
||||
else
|
||||
{:ok, code, headers} ->
|
||||
|
@ -200,7 +204,11 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do
|
|||
{:ok, data} <- client().stream_body(client),
|
||||
{:ok, duration} <- increase_read_duration(duration),
|
||||
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
|
||||
chunk_reply(conn, client, opts, sent_so_far, duration)
|
||||
else
|
||||
|
|
|
@ -53,4 +53,10 @@ def sign(%User{} = user, headers) do
|
|||
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
||||
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
|
||||
|
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Web.OStatus
|
||||
|
@ -221,12 +222,12 @@ def update_changeset(struct, params \\ %{}) do
|
|||
|> validate_length(:name, min: 1, max: name_limit)
|
||||
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)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
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
|
||||
|> cast(params, [
|
||||
|
@ -329,7 +330,13 @@ defp autofollow_users(user) do
|
|||
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
|
||||
def register(%Ecto.Changeset{} = changeset) do
|
||||
with {:ok, user} <- Repo.insert(changeset),
|
||||
{:ok, user} <- autofollow_users(user),
|
||||
{:ok, user} <- post_register_action(user) do
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
def post_register_action(%User{} = user) do
|
||||
with {:ok, user} <- autofollow_users(user),
|
||||
{:ok, user} <- set_cache(user),
|
||||
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
|
||||
{:ok, _} <- try_send_confirmation_email(user) do
|
||||
|
@ -485,6 +492,13 @@ def get_by_ap_id(ap_id) do
|
|||
Repo.get_by(User, ap_id: ap_id)
|
||||
end
|
||||
|
||||
def get_all_by_ap_id(ap_ids) do
|
||||
from(u in __MODULE__,
|
||||
where: u.ap_id in ^ap_ids
|
||||
)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
|
||||
# of the ap_id and the domain and tries to get that user
|
||||
def get_by_guessed_nickname(ap_id) do
|
||||
|
@ -742,6 +756,7 @@ def update_note_count(%User{} = user) do
|
|||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
@spec maybe_fetch_follow_information(User.t()) :: User.t()
|
||||
def maybe_fetch_follow_information(user) do
|
||||
with {:ok, user} <- fetch_follow_information(user) do
|
||||
user
|
||||
|
@ -799,9 +814,10 @@ def update_follower_count(%User{} = user) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec maybe_update_following_count(User.t()) :: User.t()
|
||||
def maybe_update_following_count(%User{local: false} = user) do
|
||||
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
{:ok, maybe_fetch_follow_information(user)}
|
||||
maybe_fetch_follow_information(user)
|
||||
else
|
||||
user
|
||||
end
|
||||
|
@ -907,6 +923,13 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
|
|||
blocker
|
||||
end
|
||||
|
||||
# clear any requested follows as well
|
||||
blocked =
|
||||
case CommonAPI.reject_follow_request(blocked, blocker) do
|
||||
{:ok, %User{} = updated_blocked} -> updated_blocked
|
||||
nil -> blocked
|
||||
end
|
||||
|
||||
blocker =
|
||||
if subscribed_to?(blocked, blocker) do
|
||||
{:ok, blocker} = unsubscribe(blocked, blocker)
|
||||
|
|
|
@ -49,6 +49,8 @@ defmodule Pleroma.User.Info do
|
|||
field(:mascot, :map, default: nil)
|
||||
field(:emoji, {:array, :map}, default: [])
|
||||
field(:pleroma_settings_store, :map, default: %{})
|
||||
field(:fields, {:array, :map}, default: nil)
|
||||
field(:raw_fields, {:array, :map}, default: [])
|
||||
|
||||
field(:notification_settings, :map,
|
||||
default: %{
|
||||
|
@ -254,11 +256,13 @@ def remote_user_creation(info, params) do
|
|||
:hide_followers,
|
||||
:hide_follows,
|
||||
:follower_count,
|
||||
:fields,
|
||||
:following_count
|
||||
])
|
||||
|> validate_fields(true)
|
||||
end
|
||||
|
||||
def user_upgrade(info, params) do
|
||||
def user_upgrade(info, params, remote? \\ false) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:ap_enabled,
|
||||
|
@ -269,8 +273,10 @@ def user_upgrade(info, params) do
|
|||
:follower_count,
|
||||
:following_count,
|
||||
:hide_follows,
|
||||
:fields,
|
||||
:hide_followers
|
||||
])
|
||||
|> validate_fields(remote?)
|
||||
end
|
||||
|
||||
def profile_update(info, params) do
|
||||
|
@ -286,10 +292,40 @@ def profile_update(info, params) do
|
|||
:background,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:fields,
|
||||
:raw_fields,
|
||||
:pleroma_settings_store
|
||||
])
|
||||
|> validate_fields()
|
||||
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()
|
||||
def confirmation_changeset(info, opts) do
|
||||
need_confirmation? = Keyword.get(opts, :need_confirmation)
|
||||
|
@ -384,6 +420,21 @@ def remove_reblog_mute(info, ap_id) do
|
|||
cast(info, params, [:muted_reblogs])
|
||||
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
|
||||
info
|
||||
|> cast(params, [
|
||||
|
|
|
@ -65,12 +65,12 @@ defp check_actor_is_active(actor) do
|
|||
if not is_nil(actor) do
|
||||
with user <- User.get_cached_by_ap_id(actor),
|
||||
false <- user.info.deactivated do
|
||||
:ok
|
||||
true
|
||||
else
|
||||
_e -> :reject
|
||||
_e -> false
|
||||
end
|
||||
else
|
||||
:ok
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -119,10 +119,10 @@ def increase_poll_votes_if_vote(%{
|
|||
|
||||
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),
|
||||
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)},
|
||||
{:ok, map} <- MRF.filter(map),
|
||||
{recipients, _, _} = get_recipients(map),
|
||||
|
@ -411,7 +411,7 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
|
|||
"actor" => 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, user}
|
||||
end
|
||||
|
@ -791,14 +791,20 @@ defp restrict_reblogs(query, _), do: query
|
|||
|
||||
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
|
||||
|
||||
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
|
||||
defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
|
||||
mutes = info.mutes
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
|
||||
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
|
||||
)
|
||||
query =
|
||||
from([activity] in query,
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
|
||||
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
|
||||
)
|
||||
|
||||
unless opts["skip_preload"] do
|
||||
from([thread_mute: tm] in query, where: is_nil(tm))
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_muted(query, _), do: query
|
||||
|
@ -899,7 +905,7 @@ defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
|
|||
|
||||
defp maybe_set_thread_muted_field(query, opts) do
|
||||
query
|
||||
|> Activity.with_set_thread_muted_field(opts["user"])
|
||||
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
|
||||
end
|
||||
|
||||
defp maybe_order(query, %{order: :desc}) do
|
||||
|
@ -1017,6 +1023,12 @@ defp object_to_user_data(data) do
|
|||
"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
|
||||
data = Transmogrifier.maybe_fix_user_object(data)
|
||||
|
||||
|
@ -1026,6 +1038,7 @@ defp object_to_user_data(data) do
|
|||
ap_enabled: true,
|
||||
source_data: data,
|
||||
banner: banner,
|
||||
fields: fields,
|
||||
locked: locked
|
||||
},
|
||||
avatar: avatar,
|
||||
|
|
|
@ -41,7 +41,7 @@ def user(conn, %{"nickname" => nickname}) do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- User.ensure_keys_present(user) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(UserView.render("user.json", %{user: user}))
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
@ -53,7 +53,7 @@ def object(conn, %{"uuid" => uuid}) do
|
|||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
{_, true} <- {:public?, Visibility.is_public?(object)} do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(ObjectView.render("object.json", %{object: object}))
|
||||
else
|
||||
{:public?, false} ->
|
||||
|
@ -69,7 +69,7 @@ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
|
|||
{page, _} = Integer.parse(page)
|
||||
|
||||
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))
|
||||
else
|
||||
{:public?, false} ->
|
||||
|
@ -83,7 +83,7 @@ def object_likes(conn, %{"uuid" => uuid}) do
|
|||
{_, true} <- {:public?, Visibility.is_public?(object)},
|
||||
likes <- Utils.get_object_likes(object) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(ObjectView.render("likes.json", ap_id, likes))
|
||||
else
|
||||
{:public?, false} ->
|
||||
|
@ -96,7 +96,7 @@ def activity(conn, %{"uuid" => uuid}) do
|
|||
%Activity{} = activity <- Activity.normalize(ap_id),
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(ObjectView.render("object.json", %{object: activity}))
|
||||
else
|
||||
{:public?, false} ->
|
||||
|
@ -104,6 +104,13 @@ def activity(conn, %{"uuid" => uuid}) do
|
|||
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
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
|
||||
|
@ -112,12 +119,12 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
|
|||
{page, _} = Integer.parse(page)
|
||||
|
||||
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}))
|
||||
else
|
||||
{:show_follows, _} ->
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> send_resp(403, "")
|
||||
end
|
||||
end
|
||||
|
@ -126,11 +133,18 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
||||
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}))
|
||||
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
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
|
||||
|
@ -139,12 +153,12 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
|
|||
{page, _} = Integer.parse(page)
|
||||
|
||||
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}))
|
||||
else
|
||||
{:show_followers, _} ->
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> send_resp(403, "")
|
||||
end
|
||||
end
|
||||
|
@ -153,7 +167,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
||||
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}))
|
||||
end
|
||||
end
|
||||
|
@ -162,7 +176,7 @@ def outbox(conn, %{"nickname" => nickname} = params) do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- User.ensure_keys_present(user) do
|
||||
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"]}))
|
||||
end
|
||||
end
|
||||
|
@ -210,7 +224,7 @@ def inbox(conn, params) do
|
|||
defp represent_service_actor(%User{} = user, conn) do
|
||||
with {:ok, user} <- User.ensure_keys_present(user) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(UserView.render("user.json", %{user: user}))
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
@ -231,7 +245,7 @@ def internal_fetch(conn, _params) do
|
|||
|
||||
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(UserView.render("user.json", %{user: user}))
|
||||
end
|
||||
|
||||
|
@ -240,7 +254,7 @@ def whoami(_conn, _params), do: {:error, :not_found}
|
|||
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
|
||||
if nickname == user.nickname do
|
||||
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"]}))
|
||||
else
|
||||
err =
|
||||
|
|
|
@ -50,9 +50,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
|||
|
||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||
|
||||
date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
signature =
|
||||
Pleroma.Signature.sign(actor, %{
|
||||
|
|
|
@ -22,13 +22,7 @@ def follow(target_instance) do
|
|||
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, _} = error ->
|
||||
Logger.error("error: #{inspect(error)}")
|
||||
error
|
||||
|
||||
e ->
|
||||
Logger.error("error: #{inspect(e)}")
|
||||
{:error, e}
|
||||
error -> format_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,16 +31,11 @@ def unfollow(target_instance) do
|
|||
with %User{} = local_user <- get_actor(),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
|
||||
{: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"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, _} = error ->
|
||||
Logger.error("error: #{inspect(error)}")
|
||||
error
|
||||
|
||||
e ->
|
||||
Logger.error("error: #{inspect(e)}")
|
||||
{:error, e}
|
||||
error -> format_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -56,11 +45,16 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
|||
%Object{} = object <- Object.normalize(activity) do
|
||||
ActivityPub.announce(user, object, nil, true, false)
|
||||
else
|
||||
e ->
|
||||
Logger.error("error: #{inspect(e)}")
|
||||
{:error, inspect(e)}
|
||||
error -> format_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -598,14 +598,20 @@ def handle_incoming(
|
|||
|
||||
banner = new_user_data[:info][:banner]
|
||||
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 =
|
||||
new_user_data
|
||||
|> Map.take([:name, :bio, :avatar])
|
||||
|> Map.put(:info, %{banner: banner, locked: locked})
|
||||
|> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
|
||||
|
||||
actor
|
||||
|> User.upgrade_changeset(update_data)
|
||||
|> User.upgrade_changeset(update_data, true)
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
ActivityPub.update(%{
|
||||
|
|
|
@ -80,6 +80,17 @@ def render("user.json", %{user: user}) do
|
|||
|> Transmogrifier.add_emoji_tags()
|
||||
|> 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,
|
||||
"type" => "Person",
|
||||
|
@ -98,6 +109,7 @@ def render("user.json", %{user: user}) do
|
|||
"publicKeyPem" => public_key
|
||||
},
|
||||
"endpoints" => endpoints,
|
||||
"attachment" => fields,
|
||||
"tag" => (user.info.source_data["tag"] || []) ++ user_tags
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
@ -12,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.AdminAPI.Config
|
||||
alias Pleroma.Web.AdminAPI.ConfigView
|
||||
alias Pleroma.Web.AdminAPI.ModerationLogView
|
||||
alias Pleroma.Web.AdminAPI.ReportView
|
||||
alias Pleroma.Web.AdminAPI.Search
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
@ -25,52 +27,113 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
action_fallback(:errors)
|
||||
|
||||
def user_delete(conn, %{"nickname" => nickname}) do
|
||||
User.get_cached_by_nickname(nickname)
|
||||
|> User.delete()
|
||||
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
User.delete(user)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
conn
|
||||
|> json(nickname)
|
||||
end
|
||||
|
||||
def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
|
||||
def user_follow(%{assigns: %{user: admin}} = conn, %{
|
||||
"follower" => follower_nick,
|
||||
"followed" => followed_nick
|
||||
}) do
|
||||
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
|
||||
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
|
||||
User.follow(follower, followed)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
followed: followed,
|
||||
follower: follower,
|
||||
action: "follow"
|
||||
})
|
||||
end
|
||||
|
||||
conn
|
||||
|> json("ok")
|
||||
end
|
||||
|
||||
def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
|
||||
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
|
||||
"follower" => follower_nick,
|
||||
"followed" => followed_nick
|
||||
}) do
|
||||
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
|
||||
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
|
||||
User.unfollow(follower, followed)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
followed: followed,
|
||||
follower: follower,
|
||||
action: "unfollow"
|
||||
})
|
||||
end
|
||||
|
||||
conn
|
||||
|> json("ok")
|
||||
end
|
||||
|
||||
def user_create(
|
||||
conn,
|
||||
%{"nickname" => nickname, "email" => email, "password" => password}
|
||||
) do
|
||||
user_data = %{
|
||||
nickname: nickname,
|
||||
name: nickname,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
bio: "."
|
||||
}
|
||||
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
|
||||
changesets =
|
||||
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
|
||||
user_data = %{
|
||||
nickname: nickname,
|
||||
name: nickname,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
bio: "."
|
||||
}
|
||||
|
||||
changeset = User.register_changeset(%User{}, user_data, need_confirmation: false)
|
||||
{:ok, user} = User.register(changeset)
|
||||
User.register_changeset(%User{}, user_data, need_confirmation: false)
|
||||
end)
|
||||
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
|
||||
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
|
||||
end)
|
||||
|
||||
conn
|
||||
|> json(user.nickname)
|
||||
case Pleroma.Repo.transaction(changesets) do
|
||||
{:ok, users} ->
|
||||
res =
|
||||
users
|
||||
|> Map.values()
|
||||
|> Enum.map(fn user ->
|
||||
{:ok, user} = User.post_register_action(user)
|
||||
|
||||
user
|
||||
end)
|
||||
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subjects: Map.values(users),
|
||||
action: "create"
|
||||
})
|
||||
|
||||
conn
|
||||
|> json(res)
|
||||
|
||||
{:error, id, changeset, _} ->
|
||||
res =
|
||||
Enum.map(changesets.operations, fn
|
||||
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
|
||||
AccountView.render("create-error.json", %{changeset: changeset})
|
||||
|
||||
{_, {:changeset, current_changeset, _}} ->
|
||||
AccountView.render("create-error.json", %{changeset: current_changeset})
|
||||
end)
|
||||
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(res)
|
||||
end
|
||||
end
|
||||
|
||||
def user_show(conn, %{"nickname" => nickname}) do
|
||||
|
@ -101,23 +164,47 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def user_toggle_activation(conn, %{"nickname" => nickname}) do
|
||||
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
|
||||
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
|
||||
|
||||
action = if user.info.deactivated, do: "activate", else: "deactivate"
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
action: action
|
||||
})
|
||||
|
||||
conn
|
||||
|> json(AccountView.render("show.json", %{user: updated_user}))
|
||||
end
|
||||
|
||||
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags) do
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
nicknames: nicknames,
|
||||
tags: tags,
|
||||
action: "tag"
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.untag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.untag(nicknames, tags) do
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
nicknames: nicknames,
|
||||
tags: tags,
|
||||
action: "untag"
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
def list_users(conn, params) do
|
||||
|
@ -158,7 +245,10 @@ defp maybe_parse_filters(filters) do
|
|||
|> Enum.into(%{}, &{&1, true})
|
||||
end
|
||||
|
||||
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
|
||||
def right_add(%{assigns: %{user: admin}} = conn, %{
|
||||
"permission_group" => permission_group,
|
||||
"nickname" => nickname
|
||||
})
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
|
||||
|
@ -173,6 +263,13 @@ def right_add(conn, %{"permission_group" => permission_group, "nickname" => nick
|
|||
|> Ecto.Changeset.change()
|
||||
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "grant",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
json(conn, info)
|
||||
|
@ -193,7 +290,7 @@ def right_get(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
|
||||
def right_delete(
|
||||
%{assigns: %{user: %User{:nickname => admin_nickname}}} = conn,
|
||||
%{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
|
||||
%{
|
||||
"permission_group" => permission_group,
|
||||
"nickname" => nickname
|
||||
|
@ -217,6 +314,13 @@ def right_delete(
|
|||
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "revoke",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
end
|
||||
|
@ -225,15 +329,33 @@ def right_delete(conn, _) do
|
|||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
|
||||
def set_activation_status(%{assigns: %{user: admin}} = conn, %{
|
||||
"nickname" => nickname,
|
||||
"status" => status
|
||||
}) do
|
||||
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
|
||||
%User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, _} <- User.deactivate(user, !status),
|
||||
do: json_response(conn, :no_content, "")
|
||||
{:ok, _} <- User.deactivate(user, !status) do
|
||||
action = if(user.info.deactivated, do: "activate", else: "deactivate")
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
action: action
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
def relay_follow(conn, %{"relay_url" => target}) do
|
||||
def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_follow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
|
||||
json(conn, target)
|
||||
else
|
||||
_ ->
|
||||
|
@ -243,8 +365,14 @@ def relay_follow(conn, %{"relay_url" => target}) do
|
|||
end
|
||||
end
|
||||
|
||||
def relay_unfollow(conn, %{"relay_url" => target}) do
|
||||
def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
|
||||
with {:ok, _message} <- Relay.unfollow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_unfollow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
|
||||
json(conn, target)
|
||||
else
|
||||
_ ->
|
||||
|
@ -335,8 +463,14 @@ def report_show(conn, %{"id" => id}) do
|
|||
end
|
||||
end
|
||||
|
||||
def report_update_state(conn, %{"id" => id, "state" => state}) do
|
||||
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
|
||||
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: report
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("show.json", %{report: report})
|
||||
|
@ -353,6 +487,13 @@ def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
|
|||
|
||||
{:ok, activity} = CommonAPI.post(user, params)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_response",
|
||||
actor: user,
|
||||
subject: activity,
|
||||
text: params["status"]
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("status.json", %{activity: activity})
|
||||
|
@ -365,8 +506,18 @@ def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def status_update(conn, %{"id" => id} = params) do
|
||||
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
|
||||
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
|
||||
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "status_update",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
sensitive: sensitive,
|
||||
visibility: params["visibility"]
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("status.json", %{activity: activity})
|
||||
|
@ -375,10 +526,26 @@ def status_update(conn, %{"id" => id} = params) do
|
|||
|
||||
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "status_delete",
|
||||
actor: user,
|
||||
subject_id: id
|
||||
})
|
||||
|
||||
json(conn, %{})
|
||||
end
|
||||
end
|
||||
|
||||
def list_log(conn, params) do
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
log = ModerationLog.get_all(page, page_size)
|
||||
|
||||
conn
|
||||
|> put_view(ModerationLogView)
|
||||
|> render("index.json", %{log: log})
|
||||
end
|
||||
|
||||
def migrate_to_db(conn, _params) do
|
||||
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
|
||||
json(conn, %{})
|
||||
|
|
|
@ -52,4 +52,50 @@ def render("invites.json", %{invites: invites}) do
|
|||
invites: render_many(invites, AccountView, "invite.json", as: :invite)
|
||||
}
|
||||
end
|
||||
|
||||
def render("created.json", %{user: user}) do
|
||||
%{
|
||||
type: "success",
|
||||
code: 200,
|
||||
data: %{
|
||||
nickname: user.nickname,
|
||||
email: user.email
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do
|
||||
%{
|
||||
type: "error",
|
||||
code: 409,
|
||||
error: parse_error(errors),
|
||||
data: %{
|
||||
nickname: Map.get(changes, :nickname),
|
||||
email: Map.get(changes, :email)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_error([]), do: ""
|
||||
|
||||
defp parse_error(errors) do
|
||||
## when nickname is duplicate ap_id constraint error is raised
|
||||
nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id)
|
||||
email_error = Keyword.get(errors, :email)
|
||||
password_error = Keyword.get(errors, :password)
|
||||
|
||||
cond do
|
||||
nickname_error ->
|
||||
"nickname #{elem(nickname_error, 0)}"
|
||||
|
||||
email_error ->
|
||||
"email #{elem(email_error, 0)}"
|
||||
|
||||
password_error ->
|
||||
"password #{elem(password_error, 0)}"
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
26
lib/pleroma/web/admin_api/views/moderation_log_view.ex
Normal file
26
lib/pleroma/web/admin_api/views/moderation_log_view.ex
Normal 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
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
defmodule Pleroma.Web.CommonAPI do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
|
@ -171,21 +173,25 @@ defp normalize_and_validate_choice_indices(choices, count) do
|
|||
end)
|
||||
end
|
||||
|
||||
def get_visibility(%{"visibility" => visibility}, in_reply_to)
|
||||
def get_visibility(_, _, %Participation{}) do
|
||||
{"direct", "direct"}
|
||||
end
|
||||
|
||||
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
|
||||
when visibility in ~w{public unlisted private direct},
|
||||
do: {visibility, get_replied_to_visibility(in_reply_to)}
|
||||
|
||||
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
|
||||
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
|
||||
visibility = {:list, String.to_integer(list_id)}
|
||||
{visibility, get_replied_to_visibility(in_reply_to)}
|
||||
end
|
||||
|
||||
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
|
||||
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
|
||||
visibility = get_replied_to_visibility(in_reply_to)
|
||||
{visibility, visibility}
|
||||
end
|
||||
|
||||
def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
|
||||
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
|
||||
|
||||
def get_replied_to_visibility(nil), do: nil
|
||||
|
||||
|
@ -195,13 +201,32 @@ def get_replied_to_visibility(activity) do
|
|||
end
|
||||
end
|
||||
|
||||
defp check_expiry_date({:ok, nil} = res), do: res
|
||||
|
||||
defp check_expiry_date({:ok, in_seconds}) do
|
||||
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
|
||||
|
||||
if ActivityExpiration.expires_late_enough?(expiry) do
|
||||
{:ok, expiry}
|
||||
else
|
||||
{:error, "Expiry date is too soon"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_expiry_date(expiry_str) do
|
||||
Ecto.Type.cast(:integer, expiry_str)
|
||||
|> check_expiry_date()
|
||||
end
|
||||
|
||||
def post(user, %{"status" => status} = data) do
|
||||
limit = Pleroma.Config.get([:instance, :limit])
|
||||
|
||||
with status <- String.trim(status),
|
||||
attachments <- attachments_from_ids(data),
|
||||
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
|
||||
{visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
|
||||
in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
|
||||
{visibility, in_reply_to_visibility} <-
|
||||
get_visibility(data, in_reply_to, in_reply_to_conversation),
|
||||
{_, false} <-
|
||||
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
|
||||
{content_html, mentions, tags} <-
|
||||
|
@ -214,10 +239,12 @@ def post(user, %{"status" => status} = data) do
|
|||
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
|
||||
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
|
||||
{poll, poll_emoji} <- make_poll_data(data),
|
||||
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
|
||||
context <- make_context(in_reply_to),
|
||||
{to, cc} <-
|
||||
get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
|
||||
context <- make_context(in_reply_to, in_reply_to_conversation),
|
||||
cw <- data["spoiler_text"] || "",
|
||||
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
|
||||
{:ok, expires_at} <- check_expiry_date(data["expires_in"]),
|
||||
full_payload <- String.trim(status <> cw),
|
||||
:ok <- validate_character_limit(full_payload, attachments, limit),
|
||||
object <-
|
||||
|
@ -243,15 +270,24 @@ def post(user, %{"status" => status} = data) do
|
|||
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
|
||||
direct? = visibility == "direct"
|
||||
|
||||
%{
|
||||
to: to,
|
||||
actor: user,
|
||||
context: context,
|
||||
object: object,
|
||||
additional: %{"cc" => cc, "directMessage" => direct?}
|
||||
}
|
||||
|> maybe_add_list_data(user, visibility)
|
||||
|> ActivityPub.create(preview?)
|
||||
result =
|
||||
%{
|
||||
to: to,
|
||||
actor: user,
|
||||
context: context,
|
||||
object: object,
|
||||
additional: %{"cc" => cc, "directMessage" => direct?}
|
||||
}
|
||||
|> maybe_add_list_data(user, visibility)
|
||||
|> ActivityPub.create(preview?)
|
||||
|
||||
if expires_at do
|
||||
with {:ok, activity} <- result do
|
||||
{:ok, _} = ActivityExpiration.create(activity, expires_at)
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
else
|
||||
{:private_to_public, true} ->
|
||||
{:error, dgettext("errors", "The message visibility must be direct")}
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
alias Calendar.Strftime
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.AuthenticationPlug
|
||||
|
@ -86,9 +87,20 @@ def attachments_from_ids_descs(ids, descs_str) do
|
|||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
|
||||
{list(String.t()), list(String.t())}
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
|
||||
@spec get_to_and_cc(
|
||||
User.t(),
|
||||
list(String.t()),
|
||||
Activity.t() | nil,
|
||||
String.t(),
|
||||
Participation.t() | nil
|
||||
) :: {list(String.t()), list(String.t())}
|
||||
|
||||
def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
|
||||
participation = Repo.preload(participation, :recipients)
|
||||
{Enum.map(participation.recipients, & &1.ap_id), []}
|
||||
end
|
||||
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
|
||||
to = [Pleroma.Constants.as_public() | mentioned_users]
|
||||
cc = [user.follower_address]
|
||||
|
||||
|
@ -99,7 +111,7 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
|
|||
end
|
||||
end
|
||||
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
|
||||
to = [user.follower_address | mentioned_users]
|
||||
cc = [Pleroma.Constants.as_public()]
|
||||
|
||||
|
@ -110,12 +122,12 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
|
|||
end
|
||||
end
|
||||
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
|
||||
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
|
||||
def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
|
||||
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
|
||||
{[user.follower_address | to], cc}
|
||||
end
|
||||
|
||||
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
|
||||
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
|
||||
if inReplyTo do
|
||||
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
|
||||
else
|
||||
|
@ -123,7 +135,7 @@ def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
|
|||
end
|
||||
end
|
||||
|
||||
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
|
||||
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
|
||||
|
||||
def get_addressed_users(_, to) when is_list(to) do
|
||||
User.get_ap_ids_by_nicknames(to)
|
||||
|
@ -253,8 +265,12 @@ defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
|
|||
|
||||
defp maybe_add_nsfw_tag(data, _), do: data
|
||||
|
||||
def make_context(%Activity{data: %{"context" => context}}), do: context
|
||||
def make_context(_), do: Utils.generate_context_id()
|
||||
def make_context(_, %Participation{} = participation) do
|
||||
Repo.preload(participation, :conversation).conversation.ap_id
|
||||
end
|
||||
|
||||
def make_context(%Activity{data: %{"context" => context}}, _), do: context
|
||||
def make_context(_, _), do: Utils.generate_context_id()
|
||||
|
||||
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
|
||||
|
||||
|
|
|
@ -33,4 +33,80 @@ defp param_to_integer(val, default) when is_binary(val) do
|
|||
end
|
||||
|
||||
defp param_to_integer(_, default), do: default
|
||||
|
||||
def add_link_headers(
|
||||
conn,
|
||||
method,
|
||||
activities,
|
||||
param \\ nil,
|
||||
params \\ %{},
|
||||
func3 \\ nil,
|
||||
func4 \\ nil
|
||||
) do
|
||||
params =
|
||||
conn.params
|
||||
|> Map.drop(["since_id", "max_id", "min_id"])
|
||||
|> Map.merge(params)
|
||||
|
||||
last = List.last(activities)
|
||||
|
||||
func3 = func3 || (&mastodon_api_url/3)
|
||||
func4 = func4 || (&mastodon_api_url/4)
|
||||
|
||||
if last do
|
||||
max_id = last.id
|
||||
|
||||
limit =
|
||||
params
|
||||
|> Map.get("limit", "20")
|
||||
|> String.to_integer()
|
||||
|
||||
min_id =
|
||||
if length(activities) <= limit do
|
||||
activities
|
||||
|> List.first()
|
||||
|> Map.get(:id)
|
||||
else
|
||||
activities
|
||||
|> Enum.at(limit * -1)
|
||||
|> Map.get(:id)
|
||||
end
|
||||
|
||||
{next_url, prev_url} =
|
||||
if param do
|
||||
{
|
||||
func4.(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
param,
|
||||
Map.merge(params, %{max_id: max_id})
|
||||
),
|
||||
func4.(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
param,
|
||||
Map.merge(params, %{min_id: min_id})
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
func3.(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
Map.merge(params, %{max_id: max_id})
|
||||
),
|
||||
func3.(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
Map.merge(params, %{min_id: min_id})
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
|
||||
|
||||
alias Ecto.Changeset
|
||||
alias Pleroma.Activity
|
||||
|
@ -137,7 +138,9 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
|
|||
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
|
||||
|
||||
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()
|
||||
|
||||
info_params =
|
||||
|
@ -156,6 +159,12 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
|
|||
end)
|
||||
end)
|
||||
|> 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 ->
|
||||
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
|
||||
end)
|
||||
|
@ -342,71 +351,6 @@ def custom_emojis(conn, _params) do
|
|||
json(conn, mastodon_emoji)
|
||||
end
|
||||
|
||||
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
|
||||
params =
|
||||
conn.params
|
||||
|> Map.drop(["since_id", "max_id", "min_id"])
|
||||
|> Map.merge(params)
|
||||
|
||||
last = List.last(activities)
|
||||
|
||||
if last do
|
||||
max_id = last.id
|
||||
|
||||
limit =
|
||||
params
|
||||
|> Map.get("limit", "20")
|
||||
|> String.to_integer()
|
||||
|
||||
min_id =
|
||||
if length(activities) <= limit do
|
||||
activities
|
||||
|> List.first()
|
||||
|> Map.get(:id)
|
||||
else
|
||||
activities
|
||||
|> Enum.at(limit * -1)
|
||||
|> Map.get(:id)
|
||||
end
|
||||
|
||||
{next_url, prev_url} =
|
||||
if param do
|
||||
{
|
||||
mastodon_api_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
param,
|
||||
Map.merge(params, %{max_id: max_id})
|
||||
),
|
||||
mastodon_api_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
param,
|
||||
Map.merge(params, %{min_id: min_id})
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
mastodon_api_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
Map.merge(params, %{max_id: max_id})
|
||||
),
|
||||
mastodon_api_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
method,
|
||||
Map.merge(params, %{min_id: min_id})
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def home_timeline(%{assigns: %{user: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|
@ -1797,7 +1741,7 @@ def conversations(%{assigns: %{user: user}} = conn, params) do
|
|||
|
||||
conversations =
|
||||
Enum.map(participations, fn participation ->
|
||||
ConversationView.render("participation.json", %{participation: participation, user: user})
|
||||
ConversationView.render("participation.json", %{participation: participation, for: user})
|
||||
end)
|
||||
|
||||
conn
|
||||
|
@ -1810,7 +1754,7 @@ def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_
|
|||
Repo.get_by(Participation, id: participation_id, user_id: user.id),
|
||||
{:ok, participation} <- Participation.mark_as_read(participation) do
|
||||
participation_view =
|
||||
ConversationView.render("participation.json", %{participation: participation, user: user})
|
||||
ConversationView.render("participation.json", %{participation: participation, for: user})
|
||||
|
||||
conn
|
||||
|> json(participation_view)
|
||||
|
|
|
@ -94,12 +94,18 @@ defp do_render("account.json", %{user: user} = opts) do
|
|||
end)
|
||||
|
||||
fields =
|
||||
(user.info.source_data["attachment"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
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)
|
||||
|
||||
raw_fields = Map.get(user.info, :raw_fields, [])
|
||||
|
||||
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
|
||||
|
||||
relationship = render("relationship.json", %{user: opts[:for], target: user})
|
||||
|
||||
%{
|
||||
|
@ -124,6 +130,7 @@ defp do_render("account.json", %{user: user} = opts) do
|
|||
source: %{
|
||||
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
|
||||
sensitive: false,
|
||||
fields: raw_fields,
|
||||
pleroma: %{}
|
||||
},
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
|
|||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("participation.json", %{participation: participation, user: user}) do
|
||||
participation = Repo.preload(participation, conversation: :users)
|
||||
def render("participation.json", %{participation: participation, for: user}) do
|
||||
participation = Repo.preload(participation, conversation: [], recipients: [])
|
||||
|
||||
last_activity_id =
|
||||
with nil <- participation.last_activity_id do
|
||||
|
@ -28,7 +28,7 @@ def render("participation.json", %{participation: participation, user: user}) do
|
|||
|
||||
# Conversations return all users except the current user.
|
||||
users =
|
||||
participation.conversation.users
|
||||
participation.recipients
|
||||
|> Enum.reject(&(&1.id == user.id))
|
||||
|
||||
accounts =
|
||||
|
|
|
@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
@ -175,6 +178,15 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
|
|||
|
||||
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
|
||||
|
||||
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
|
||||
|
||||
expires_at =
|
||||
with true <- client_posted_this_activity,
|
||||
expiration when not is_nil(expiration) <-
|
||||
ActivityExpiration.get_by_activity_id(activity.id) do
|
||||
expiration.scheduled_at
|
||||
end
|
||||
|
||||
thread_muted? =
|
||||
case activity.thread_muted? do
|
||||
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
|
||||
|
@ -235,6 +247,19 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
|
|||
object.data["url"] || object.data["external_url"] || object.data["id"]
|
||||
end
|
||||
|
||||
direct_conversation_id =
|
||||
with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
|
||||
{_, %User{} = for_user} <- {:for_user, opts[:for]},
|
||||
%{data: %{"context" => context}} when is_binary(context) <- activity,
|
||||
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
|
||||
%Participation{id: participation_id} <-
|
||||
Participation.for_user_and_conversation(for_user, conversation) do
|
||||
participation_id
|
||||
else
|
||||
_e ->
|
||||
nil
|
||||
end
|
||||
|
||||
%{
|
||||
id: to_string(activity.id),
|
||||
uri: object.data["id"],
|
||||
|
@ -272,7 +297,9 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
|
|||
conversation_id: get_context_id(activity),
|
||||
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
|
||||
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
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
@ -22,6 +22,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
alias Pleroma.Web.Router
|
||||
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(
|
||||
|
@ -32,8 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
action_fallback(:errors)
|
||||
|
||||
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
|
||||
with {_, %User{} = user} <-
|
||||
{:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
|
||||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
|
||||
RedirectController.redirector_with_meta(conn, %{user: user})
|
||||
end
|
||||
end
|
||||
|
|
73
lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
Normal file
73
lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
|
||||
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.MastodonAPI.ConversationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
|
||||
with %Participation{} = participation <- Participation.get(participation_id),
|
||||
true <- user.id == participation.user_id do
|
||||
conn
|
||||
|> put_view(ConversationView)
|
||||
|> render("participation.json", %{participation: participation, for: user})
|
||||
end
|
||||
end
|
||||
|
||||
def conversation_statuses(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"id" => participation_id} = params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|
||||
participation =
|
||||
participation_id
|
||||
|> Participation.get(preload: [:conversation])
|
||||
|
||||
if user.id == participation.user_id do
|
||||
activities =
|
||||
participation.conversation.ap_id
|
||||
|> ActivityPub.fetch_activities_for_context(params)
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> add_link_headers(
|
||||
:conversation_statuses,
|
||||
activities,
|
||||
participation_id,
|
||||
params,
|
||||
nil,
|
||||
&pleroma_api_url/4
|
||||
)
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def update_conversation(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"id" => participation_id, "recipients" => recipients}
|
||||
) do
|
||||
participation =
|
||||
participation_id
|
||||
|> Participation.get()
|
||||
|
||||
with true <- user.id == participation.user_id,
|
||||
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
|
||||
conn
|
||||
|> put_view(ConversationView)
|
||||
|> render("participation.json", %{participation: participation, for: user})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -133,6 +133,10 @@ defmodule Pleroma.Web.Router do
|
|||
})
|
||||
end
|
||||
|
||||
pipeline :http_signature do
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
|
||||
pipe_through(:pleroma_api)
|
||||
|
||||
|
@ -155,7 +159,7 @@ defmodule Pleroma.Web.Router do
|
|||
post("/users/unfollow", AdminAPIController, :user_unfollow)
|
||||
|
||||
delete("/users", AdminAPIController, :user_delete)
|
||||
post("/users", AdminAPIController, :user_create)
|
||||
post("/users", AdminAPIController, :users_create)
|
||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||
put("/users/tag", AdminAPIController, :tag_users)
|
||||
delete("/users/tag", AdminAPIController, :untag_users)
|
||||
|
@ -198,6 +202,8 @@ defmodule Pleroma.Web.Router do
|
|||
post("/config", AdminAPIController, :config_update)
|
||||
get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
|
||||
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
|
||||
|
||||
get("/moderation_log", AdminAPIController, :list_log)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.TwitterAPI do
|
||||
|
@ -259,6 +265,21 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
end
|
||||
|
||||
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_read)
|
||||
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
||||
get("/conversations/:id", PleromaAPIController, :conversation)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_write)
|
||||
patch("/conversations/:id", PleromaAPIController, :update_conversation)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
|
@ -671,7 +692,14 @@ defmodule Pleroma.Web.Router do
|
|||
pipe_through(:ap_service_actor)
|
||||
|
||||
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
|
||||
|
||||
scope "/internal/fetch", Pleroma.Web.ActivityPub do
|
||||
|
|
|
@ -113,8 +113,7 @@ def handle_cast(
|
|||
|> Map.get("#{topic}:#{item.user_id}", [])
|
||||
|> Enum.each(fn socket ->
|
||||
with %User{} = user <- User.get_cached_by_ap_id(socket.assigns[:user].ap_id),
|
||||
true <- should_send?(user, item),
|
||||
false <- CommonAPI.thread_muted?(user, item.activity) do
|
||||
true <- should_send?(user, item) do
|
||||
send(
|
||||
socket.transport_pid,
|
||||
{:text, represent_notification(socket.assigns[:user], item)}
|
||||
|
@ -202,7 +201,7 @@ def represent_conversation(%Participation{} = participation) do
|
|||
payload:
|
||||
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
|
||||
participation: participation,
|
||||
user: participation.user
|
||||
for: participation.user
|
||||
})
|
||||
|> Jason.encode!()
|
||||
}
|
||||
|
@ -236,7 +235,8 @@ defp should_send?(%User{} = user, %Activity{} = item) do
|
|||
%{host: parent_host} <- URI.parse(parent.data["actor"]),
|
||||
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
|
||||
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
|
||||
true <- thread_containment(item, user) do
|
||||
true <- thread_containment(item, user),
|
||||
false <- CommonAPI.thread_muted?(user, item) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
|
|
|
@ -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>
|
||||
<ul>
|
||||
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
|
||||
<li><%= link from.nickname, to: mention.activity.actor %>: <%= raw object.data["content"] %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
|
||||
<%= if @followers != [] do %>
|
||||
<h2><%= length(@followers) %> New Followers:</h2>
|
||||
<ul>
|
||||
<%= for %{data: follow, from: from} <- @followers do %>
|
||||
<li><%= link from.nickname, to: follow.activity.actor %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<head>
|
||||
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta content="width=device-width" name="viewport" />
|
||||
<!--[if !mso]><!-->
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
|
||||
<!--<![endif]-->
|
||||
<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>
|
||||
<p>The email address you are subscribed as is <%= @user.email %>. To unsubscribe, please go <%= link "here", to: @unsubscribe_link %>.</p>
|
||||
a {
|
||||
|
||||
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>
|
||||
|
|
|
@ -74,12 +74,15 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do
|
|||
|> HTML.filter_tags(User.html_filter_policy(for_user))
|
||||
|> Formatter.emojify(emoji)
|
||||
|
||||
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
|
||||
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
|
||||
fields =
|
||||
(user.info.source_data["attachment"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
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)
|
||||
|
||||
data =
|
||||
%{
|
||||
|
|
|
@ -2,4 +2,14 @@ defmodule Pleroma.Web.EmailView do
|
|||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
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
|
||||
|
|
|
@ -6,7 +6,7 @@ def change do
|
|||
add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
|
||||
add :context, :string
|
||||
end
|
||||
|
||||
|
||||
create_if_not_exists unique_index(:thread_mutes, [:user_id, :context], name: :unique_index)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateConversationParticipationRecipientShips do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:conversation_participation_recipient_ships) do
|
||||
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
add(:participation_id, references(:conversation_participations, on_delete: :delete_all))
|
||||
end
|
||||
|
||||
create_if_not_exists index(:conversation_participation_recipient_ships, [:user_id])
|
||||
create_if_not_exists index(:conversation_participation_recipient_ships, [:participation_id])
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
27
test/activity_expiration_test.exs
Normal file
27
test/activity_expiration_test.exs
Normal 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
|
17
test/activity_expiration_worker_test.exs
Normal file
17
test/activity_expiration_worker_test.exs
Normal 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
|
|
@ -164,4 +164,13 @@ test "find all statuses for unauthenticated users when `limit_to_local_content`
|
|||
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
|
||||
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
|
||||
|
|
|
@ -5,14 +5,8 @@
|
|||
defmodule Pleroma.Config.TransferTaskTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
setup do
|
||||
dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
|
||||
|
||||
clear_config([:instance, :dynamic_configuration]) do
|
||||
Pleroma.Config.put([:instance, :dynamic_configuration], true)
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
|
||||
end)
|
||||
end
|
||||
|
||||
test "transfer config values from db to env" do
|
||||
|
|
|
@ -8,6 +8,50 @@ defmodule Pleroma.Conversation.ParticipationTest do
|
|||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
test "getting a participation will also preload things" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||
|
||||
[participation] = Participation.for_user(user)
|
||||
|
||||
participation = Participation.get(participation.id, preload: [:conversation])
|
||||
|
||||
assert %Pleroma.Conversation{} = participation.conversation
|
||||
end
|
||||
|
||||
test "for a new conversation, it sets the recipents of the participation" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
third_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||
|
||||
[participation] = Participation.for_user(user)
|
||||
participation = Pleroma.Repo.preload(participation, :recipients)
|
||||
|
||||
assert length(participation.recipients) == 2
|
||||
assert user in participation.recipients
|
||||
assert other_user in participation.recipients
|
||||
|
||||
# Mentioning another user in the same conversation will not add a new recipients.
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{
|
||||
"in_reply_to_status_id" => activity.id,
|
||||
"status" => "Hey @#{third_user.nickname}.",
|
||||
"visibility" => "direct"
|
||||
})
|
||||
|
||||
[participation] = Participation.for_user(user)
|
||||
participation = Pleroma.Repo.preload(participation, :recipients)
|
||||
|
||||
assert length(participation.recipients) == 2
|
||||
end
|
||||
|
||||
test "it creates a participation for a conversation and a user" do
|
||||
user = insert(:user)
|
||||
conversation = insert(:conversation)
|
||||
|
@ -102,4 +146,23 @@ test "Doesn't die when the conversation gets empty" do
|
|||
|
||||
[] = Participation.for_user_with_last_activity_id(user)
|
||||
end
|
||||
|
||||
test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
|
||||
[participation] = Participation.for_user_with_last_activity_id(user)
|
||||
|
||||
participation = Repo.preload(participation, :recipients)
|
||||
|
||||
assert participation.recipients |> length() == 1
|
||||
assert user in participation.recipients
|
||||
|
||||
{:ok, participation} = Participation.set_recipients(participation, [other_user.id])
|
||||
|
||||
assert participation.recipients |> length() == 2
|
||||
assert user in participation.recipients
|
||||
assert other_user in participation.recipients
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,14 +11,8 @@ defmodule Pleroma.ConversationTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
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
|
||||
clear_config_all([:instance, :federating]) do
|
||||
Pleroma.Config.put([:instance, :federating], true)
|
||||
end
|
||||
|
||||
test "it goes through old direct conversations" do
|
||||
|
|
|
@ -15,11 +15,7 @@ defmodule Pleroma.Emails.MailerTest do
|
|||
to: [{"Test User", "user1@example.com"}]
|
||||
}
|
||||
|
||||
setup do
|
||||
value = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
|
||||
on_exit(fn -> Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], value) end)
|
||||
:ok
|
||||
end
|
||||
clear_config([Pleroma.Emails.Mailer, :enabled])
|
||||
|
||||
test "not send email when mailer is disabled" do
|
||||
Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false)
|
||||
|
|
36
test/fixtures/mastodon-update.json
vendored
36
test/fixtures/mastodon-update.json
vendored
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"type": "Update",
|
||||
"object": {
|
||||
"url": "http://mastodon.example.org/@gargron",
|
||||
"type": "Person",
|
||||
"summary": "<p>Some bio</p>",
|
||||
"publicKey": {
|
||||
{
|
||||
"type": "Update",
|
||||
"object": {
|
||||
"url": "http://mastodon.example.org/@gargron",
|
||||
"type": "Person",
|
||||
"summary": "<p>Some bio</p>",
|
||||
"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",
|
||||
"owner": "http://mastodon.example.org/users/gargron",
|
||||
"id": "http://mastodon.example.org/users/gargron#main-key"
|
||||
|
@ -20,7 +20,27 @@
|
|||
"endpoints": {
|
||||
"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",
|
||||
"actor": "http://mastodon.example.org/users/gargron",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,21 +4,19 @@
|
|||
|
||||
defmodule Pleroma.HTTP.RequestBuilderTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Pleroma.Tests.Helpers
|
||||
alias Pleroma.HTTP.RequestBuilder
|
||||
|
||||
describe "headers/2" do
|
||||
clear_config([:http, :send_user_agent])
|
||||
|
||||
test "don't send pleroma user agent" do
|
||||
assert RequestBuilder.headers(%{}, []) == %{headers: []}
|
||||
end
|
||||
|
||||
test "send pleroma user agent" do
|
||||
send = Pleroma.Config.get([:http, :send_user_agent])
|
||||
Pleroma.Config.put([:http, :send_user_agent], true)
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([:http, :send_user_agent], send)
|
||||
end)
|
||||
|
||||
assert RequestBuilder.headers(%{}, []) == %{
|
||||
headers: [{"User-Agent", Pleroma.Application.user_agent()}]
|
||||
}
|
||||
|
|
301
test/moderation_log_test.exs
Normal file
301
test/moderation_log_test.exs
Normal 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
|
|
@ -159,32 +159,28 @@ test "it can refetch pruned objects" do
|
|||
end
|
||||
|
||||
describe "signed fetches" do
|
||||
clear_config([:activitypub, :sign_object_fetches])
|
||||
|
||||
test_with_mock "it signs fetches when configured to do so",
|
||||
Pleroma.Signature,
|
||||
[:passthrough],
|
||||
[] do
|
||||
option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
|
||||
Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
|
||||
|
||||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
||||
|
||||
assert called(Pleroma.Signature.sign(:_, :_))
|
||||
|
||||
Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
|
||||
end
|
||||
|
||||
test_with_mock "it doesn't sign fetches when not configured to do so",
|
||||
Pleroma.Signature,
|
||||
[:passthrough],
|
||||
[] do
|
||||
option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
|
||||
Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
|
||||
|
||||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
||||
|
||||
refute called(Pleroma.Signature.sign(:_, :_))
|
||||
|
||||
Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,8 +9,10 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
|
|||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.User
|
||||
|
||||
clear_config([:instance, :public])
|
||||
|
||||
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
|
||||
|
@ -21,7 +23,7 @@ test "it halts if not public and no user is assigned", %{conn: conn} do
|
|||
end
|
||||
|
||||
test "it continues if public", %{conn: conn} do
|
||||
set_public_to(true)
|
||||
Config.put([:instance, :public], true)
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|
@ -31,7 +33,7 @@ test "it continues if public", %{conn: conn} do
|
|||
end
|
||||
|
||||
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
|
||||
|
@ -43,13 +45,4 @@ test "it continues if a user is assigned, even if not public", %{conn: conn} do
|
|||
|
||||
assert ret_conn == conn
|
||||
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
|
||||
|
|
|
@ -7,17 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
|
|||
alias Pleroma.Config
|
||||
alias Plug.Conn
|
||||
|
||||
clear_config([:http_securiy, :enabled])
|
||||
clear_config([:http_security, :sts])
|
||||
|
||||
describe "http security enabled" do
|
||||
setup do
|
||||
enabled = Config.get([:http_securiy, :enabled])
|
||||
|
||||
Config.put([:http_security, :enabled], true)
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put([:http_security, :enabled], enabled)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it sends CSP headers when enabled", %{conn: conn} do
|
||||
|
@ -81,14 +76,8 @@ test "it sends `report-to` & `report-uri` CSP response headers" do
|
|||
end
|
||||
|
||||
test "it does not send CSP headers when disabled", %{conn: conn} do
|
||||
enabled = Config.get([:http_securiy, :enabled])
|
||||
|
||||
Config.put([:http_security, :enabled], false)
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put([:http_security, :enabled], enabled)
|
||||
end)
|
||||
|
||||
conn = get(conn, "/api/v1/instance")
|
||||
|
||||
assert Conn.get_resp_header(conn, "x-xss-protection") == []
|
||||
|
|
|
@ -8,14 +8,12 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do
|
|||
@dir "test/tmp/instance_static"
|
||||
|
||||
setup do
|
||||
static_dir = Pleroma.Config.get([:instance, :static_dir])
|
||||
Pleroma.Config.put([:instance, :static_dir], @dir)
|
||||
File.mkdir_p!(@dir)
|
||||
on_exit(fn -> File.rm_rf(@dir) end)
|
||||
end
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([:instance, :static_dir], static_dir)
|
||||
File.rm_rf(@dir)
|
||||
end)
|
||||
clear_config([:instance, :static_dir]) do
|
||||
Pleroma.Config.put([:instance, :static_dir], @dir)
|
||||
end
|
||||
|
||||
test "overrides index" do
|
||||
|
|
|
@ -108,11 +108,11 @@ defp stream_mock(invokes, with_close? \\ false) do
|
|||
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)
|
||||
|
||||
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) =~
|
||||
"[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do
|
|||
import ExUnit.CaptureLog
|
||||
import Pleroma.Factory
|
||||
import Tesla.Mock
|
||||
import Mock
|
||||
|
||||
alias Pleroma.Signature
|
||||
|
||||
|
@ -114,4 +115,17 @@ test "it properly deduces the actor id for mastodon and pleroma" do
|
|||
"https://example.com/users/1234"
|
||||
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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
defmodule Pleroma.Factory do
|
||||
|
@ -143,6 +143,25 @@ def note_activity_factory(attrs \\ %{}) do
|
|||
|> Map.merge(attrs)
|
||||
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
|
||||
article = insert(:article)
|
||||
|
||||
|
|
|
@ -7,8 +7,52 @@ defmodule Pleroma.Tests.Helpers do
|
|||
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
|
||||
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
|
||||
collection
|
||||
|> Enum.map(& &1.id)
|
||||
|
@ -30,6 +74,15 @@ def render_json(view, template, assigns) do
|
|||
|> Poison.encode!()
|
||||
|> Poison.decode!()
|
||||
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
|
||||
|
|
|
@ -17,9 +17,12 @@ def request(
|
|||
with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do
|
||||
res
|
||||
else
|
||||
{_, _r} = error ->
|
||||
# Logger.warn(r)
|
||||
error
|
||||
error ->
|
||||
with {:error, message} <- error do
|
||||
Logger.warn(message)
|
||||
end
|
||||
|
||||
{_, _r} = error
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -968,9 +971,25 @@ def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp",
|
|||
}}
|
||||
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
|
||||
{: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)
|
||||
}"}
|
||||
end
|
||||
|
@ -1032,7 +1051,10 @@ def post("http://404.site" <> _, _, _, _) do
|
|||
}}
|
||||
end
|
||||
|
||||
def post(url, _query, _body, _headers) do
|
||||
{:error, "Not implemented the mock response for post #{inspect(url)}"}
|
||||
def post(url, query, body, headers) do
|
||||
{:error,
|
||||
"Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{
|
||||
inspect(headers)
|
||||
}"}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,21 +11,20 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
Mix.shell(Mix.Shell.Process)
|
||||
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 ->
|
||||
Mix.shell(Mix.Shell.IO)
|
||||
Application.delete_env(:pleroma, :first_setting)
|
||||
Application.delete_env(:pleroma, :second_setting)
|
||||
Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
|
||||
:ok = File.rm(temp_file)
|
||||
end)
|
||||
|
||||
{:ok, temp_file: temp_file}
|
||||
end
|
||||
|
||||
clear_config_all([:instance, :dynamic_configuration]) do
|
||||
Pleroma.Config.put([:instance, :dynamic_configuration], true)
|
||||
end
|
||||
|
||||
test "settings are migrated to db" do
|
||||
assert Repo.all(Config) == []
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -22,6 +23,52 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
:ok
|
||||
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
|
||||
test "following and followers count are updated" do
|
||||
[user, user2] = insert_pair(:user)
|
||||
|
|
|
@ -44,7 +44,7 @@ test "Sends digest to the given user" do
|
|||
|
||||
assert_email_sent(
|
||||
to: {user2.name, user2.email},
|
||||
html_body: ~r/new mentions:/i
|
||||
html_body: ~r/here is what you've missed!/i
|
||||
)
|
||||
end
|
||||
end
|
|
@ -50,7 +50,8 @@ test "relay is unfollowed" do
|
|||
%User{ap_id: follower_id} = local_user = Relay.get_actor()
|
||||
target_user = User.get_cached_by_ap_id(target_instance)
|
||||
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])
|
||||
|
||||
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
|
||||
|
@ -67,6 +68,30 @@ test "relay is unfollowed" do
|
|||
assert undo_activity.data["type"] == "Undo"
|
||||
assert undo_activity.data["actor"] == local_user.ap_id
|
||||
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
|
||||
|
|
|
@ -4,17 +4,17 @@
|
|||
|
||||
defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
|
||||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
alias Mix.Tasks.Pleroma.RobotsTxt
|
||||
|
||||
clear_config([:instance, :static_dir])
|
||||
|
||||
test "creates new dir" do
|
||||
path = "test/fixtures/new_dir/"
|
||||
file_path = path <> "robots.txt"
|
||||
|
||||
static_dir = Pleroma.Config.get([:instance, :static_dir])
|
||||
Pleroma.Config.put([:instance, :static_dir], path)
|
||||
|
||||
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)
|
||||
end)
|
||||
|
||||
|
@ -29,11 +29,9 @@ test "creates new dir" do
|
|||
test "to existance folder" do
|
||||
path = "test/fixtures/"
|
||||
file_path = path <> "robots.txt"
|
||||
static_dir = Pleroma.Config.get([:instance, :static_dir])
|
||||
Pleroma.Config.put([:instance, :static_dir], path)
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([:instance, :static_dir], static_dir)
|
||||
:ok = File.rm(file_path)
|
||||
end)
|
||||
|
||||
|
|
|
@ -9,12 +9,6 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
|
|||
alias Pleroma.Upload
|
||||
|
||||
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{
|
||||
name: "an… image.jpg",
|
||||
content_type: "image/jpg",
|
||||
|
@ -24,6 +18,8 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
|
|||
%{upload_file: upload_file}
|
||||
end
|
||||
|
||||
clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
|
||||
|
||||
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
|
||||
Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
|
||||
{:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
|
||||
|
|
|
@ -10,13 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
|
|||
alias Pleroma.Upload
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
setup do
|
||||
filter = Config.get([Filter.Mogrify, :args])
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put([Filter.Mogrify, :args], filter)
|
||||
end)
|
||||
end
|
||||
clear_config([Filter.Mogrify, :args])
|
||||
|
||||
test "apply mogrify filter" do
|
||||
Config.put([Filter.Mogrify, :args], [{"tint", "40"}])
|
||||
|
|
|
@ -8,13 +8,7 @@ defmodule Pleroma.Upload.FilterTest do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
setup do
|
||||
custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text])
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename)
|
||||
end)
|
||||
end
|
||||
clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
|
||||
|
||||
test "applies filters" do
|
||||
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
|
||||
|
|
|
@ -250,12 +250,8 @@ test "escapes reserved uri characters" do
|
|||
end
|
||||
|
||||
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")
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([Pleroma.Upload, :base_url], nil)
|
||||
end)
|
||||
end
|
||||
|
||||
test "returns a media url with configured base_url" do
|
||||
|
|
|
@ -11,19 +11,11 @@ defmodule Pleroma.Uploaders.S3Test do
|
|||
import Mock
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
setup do
|
||||
config = Config.get([Pleroma.Uploaders.S3])
|
||||
|
||||
clear_config([Pleroma.Uploaders.S3]) do
|
||||
Config.put([Pleroma.Uploaders.S3],
|
||||
bucket: "test_bucket",
|
||||
public_endpoint: "https://s3.amazonaws.com"
|
||||
)
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put([Pleroma.Uploaders.S3], config)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "get_file/1" do
|
||||
|
|
|
@ -21,6 +21,8 @@ defmodule Pleroma.UserTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
clear_config([:instance, :account_activation_required])
|
||||
|
||||
describe "when tags are nil" do
|
||||
test "tagging a user" do
|
||||
user = insert(:user, %{tags: nil})
|
||||
|
@ -90,6 +92,17 @@ test "doesn't return already accepted or duplicate follow requests" do
|
|||
assert activity
|
||||
end
|
||||
|
||||
test "clears follow requests when requester is blocked" do
|
||||
followed = insert(:user, %{info: %{locked: true}})
|
||||
follower = insert(:user)
|
||||
|
||||
CommonAPI.follow(follower, followed)
|
||||
assert {:ok, [_activity]} = User.get_follow_requests(followed)
|
||||
|
||||
{:ok, _follower} = User.block(followed, follower)
|
||||
assert {:ok, []} = User.get_follow_requests(followed)
|
||||
end
|
||||
|
||||
test "follow_all follows mutliple users" do
|
||||
user = insert(:user)
|
||||
followed_zero = insert(:user)
|
||||
|
@ -192,24 +205,64 @@ test "local users do not automatically follow local locked accounts" do
|
|||
# assert websub
|
||||
# end
|
||||
|
||||
test "unfollow takes a user and another user" do
|
||||
followed = insert(:user)
|
||||
user = insert(:user, %{following: [User.ap_followers(followed)]})
|
||||
describe "unfollow/2" do
|
||||
setup do
|
||||
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 == []
|
||||
end
|
||||
test "unfollow with syncronizes external user" do
|
||||
Pleroma.Config.put([:instance, :external_user_synchronization], true)
|
||||
|
||||
test "unfollow doesn't unfollow yourself" do
|
||||
user = insert(:user)
|
||||
followed =
|
||||
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)
|
||||
assert user.following == [user.ap_id]
|
||||
{:ok, user, _activity} = User.unfollow(user, followed)
|
||||
|
||||
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
|
||||
|
||||
test "test if a user is following another user" do
|
||||
|
@ -236,6 +289,9 @@ test "fetches correct profile for nickname beginning with number" do
|
|||
password_confirmation: "test",
|
||||
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
|
||||
user = insert(:user)
|
||||
|
@ -252,8 +308,6 @@ test "it autofollows accounts that are set for it" do
|
|||
|
||||
assert User.following?(registered_user, user)
|
||||
refute User.following?(registered_user, remote_user)
|
||||
|
||||
Pleroma.Config.put([:instance, :autofollowed_nicknames], [])
|
||||
end
|
||||
|
||||
test "it sends a welcome message if it is set" do
|
||||
|
@ -269,9 +323,6 @@ test "it sends a welcome message if it is set" do
|
|||
assert registered_user.ap_id in activity.recipients
|
||||
assert Object.normalize(activity).data["content"] =~ "cool site"
|
||||
assert activity.actor == welcome_user.ap_id
|
||||
|
||||
Pleroma.Config.put([:instance, :welcome_user_nickname], nil)
|
||||
Pleroma.Config.put([:instance, :welcome_message], nil)
|
||||
end
|
||||
|
||||
test "it requires an email, name, nickname and password, bio is optional" do
|
||||
|
@ -337,15 +388,8 @@ test "it ensures info is not nil" do
|
|||
email: "email@example.com"
|
||||
}
|
||||
|
||||
setup do
|
||||
setting = Pleroma.Config.get([:instance, :account_activation_required])
|
||||
|
||||
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
|
||||
clear_config([:instance, :account_activation_required]) do
|
||||
Pleroma.Config.put([:instance, :account_activation_required], true)
|
||||
end
|
||||
|
||||
test "it creates unconfirmed user" do
|
||||
|
@ -497,6 +541,9 @@ test "returns an ap_followers link for a user" do
|
|||
avatar: %{some: "avatar"}
|
||||
}
|
||||
|
||||
clear_config([:instance, :user_bio_length])
|
||||
clear_config([:instance, :user_name_length])
|
||||
|
||||
test "it confirms validity" do
|
||||
cs = User.remote_user_creation(@valid_remote)
|
||||
assert cs.valid?
|
||||
|
@ -992,6 +1039,8 @@ test "hide a user's statuses from timelines and notifications" do
|
|||
[user: user]
|
||||
end
|
||||
|
||||
clear_config([:instance, :federating])
|
||||
|
||||
test ".delete_user_activities deletes all create activities", %{user: user} do
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
|
||||
|
||||
|
@ -1001,6 +1050,13 @@ test ".delete_user_activities deletes all create activities", %{user: user} do
|
|||
refute Activity.get_by_id(activity.id)
|
||||
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
|
||||
follower = insert(:user)
|
||||
{:ok, follower} = User.follow(follower, user)
|
||||
|
@ -1042,9 +1098,7 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
|
|||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
config_path = [:instance, :federating]
|
||||
initial_setting = Pleroma.Config.get(config_path)
|
||||
Pleroma.Config.put(config_path, true)
|
||||
Pleroma.Config.put([:instance, :federating], true)
|
||||
|
||||
{:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
|
||||
{:ok, _} = User.follow(follower, user)
|
||||
|
@ -1056,8 +1110,6 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
|
|||
inbox: "http://mastodon.example.org/inbox"
|
||||
})
|
||||
)
|
||||
|
||||
Pleroma.Config.put(config_path, initial_setting)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1123,8 +1175,6 @@ test "auth_active?/1 works correctly" do
|
|||
refute User.auth_active?(local_user)
|
||||
assert User.auth_active?(confirmed_user)
|
||||
assert User.auth_active?(remote_user)
|
||||
|
||||
Pleroma.Config.put([:instance, :account_activation_required], false)
|
||||
end
|
||||
|
||||
describe "superuser?/1" do
|
||||
|
@ -1169,8 +1219,6 @@ test "returns false when the account is unauthenticated and auth is required" do
|
|||
other_user = insert(:user, local: true)
|
||||
|
||||
refute User.visible_for?(user, other_user)
|
||||
|
||||
Pleroma.Config.put([:instance, :account_activation_required], false)
|
||||
end
|
||||
|
||||
test "returns true when the account is unauthenticated and auth is not required" do
|
||||
|
@ -1187,8 +1235,6 @@ test "returns true when the account is unauthenticated and being viewed by a pri
|
|||
other_user = insert(:user, local: true, info: %{is_admin: true})
|
||||
|
||||
assert User.visible_for?(user, other_user)
|
||||
|
||||
Pleroma.Config.put([:instance, :account_activation_required], false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1207,18 +1253,18 @@ test "preserves hosts in user links text" do
|
|||
end
|
||||
|
||||
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>"
|
||||
assert expected_text == User.parse_bio(bio, user)
|
||||
|
||||
bio = "http://example.org/rel_me/link"
|
||||
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
|
||||
bio = "http://example.com/rel_me/link"
|
||||
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
|
||||
assert expected_text == User.parse_bio(bio, user)
|
||||
|
||||
bio = "http://example.org/rel_me/anchor"
|
||||
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
|
||||
bio = "http://example.com/rel_me/anchor"
|
||||
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
|
||||
assert expected_text == User.parse_bio(bio, user)
|
||||
end
|
||||
end
|
||||
|
@ -1501,10 +1547,7 @@ test "performs update cache if user updated" do
|
|||
end
|
||||
|
||||
describe "following/followers synchronization" do
|
||||
setup do
|
||||
sync = Pleroma.Config.get([:instance, :external_user_synchronization])
|
||||
on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end)
|
||||
end
|
||||
clear_config([:instance, :external_user_synchronization])
|
||||
|
||||
test "updates the counters normally on following/getting a follow when disabled" do
|
||||
Pleroma.Config.put([:instance, :external_user_synchronization], false)
|
||||
|
|
|
@ -10,23 +10,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
setup_all do
|
||||
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
|
||||
end
|
||||
|
||||
clear_config_all([:instance, :federating],
|
||||
do: Pleroma.Config.put([:instance, :federating], true)
|
||||
)
|
||||
|
||||
describe "/relay" do
|
||||
clear_config([:instance, :allow_relay])
|
||||
|
||||
test "with the relay active, it returns the relay user", %{conn: conn} do
|
||||
res =
|
||||
conn
|
||||
|
@ -43,8 +43,6 @@ test "with the relay disabled, it returns 404", %{conn: conn} do
|
|||
|> get(activity_pub_path(conn, :relay))
|
||||
|> json_response(404)
|
||||
|> assert
|
||||
|
||||
Pleroma.Config.put([:instance, :allow_relay], true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -596,6 +594,34 @@ test "it increases like count when receiving a like action", %{conn: conn} do
|
|||
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
|
||||
test "it returns the followers in a collection", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
|
|
@ -538,6 +538,29 @@ test "doesn't return muted activities" do
|
|||
assert Enum.member?(activities, activity_one)
|
||||
end
|
||||
|
||||
test "doesn't return thread muted activities" do
|
||||
user = insert(:user)
|
||||
_activity_one = insert(:note_activity)
|
||||
note_two = insert(:note, data: %{"context" => "suya.."})
|
||||
activity_two = insert(:note_activity, note: note_two)
|
||||
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
|
||||
|
||||
assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
|
||||
end
|
||||
|
||||
test "returns thread muted activities when with_muted is set" do
|
||||
user = insert(:user)
|
||||
_activity_one = insert(:note_activity)
|
||||
note_two = insert(:note, data: %{"context" => "suya.."})
|
||||
activity_two = insert(:note_activity, note: note_two)
|
||||
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
|
||||
|
||||
assert [_activity_two, _activity_one] =
|
||||
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
|
||||
end
|
||||
|
||||
test "does include announces on request" do
|
||||
activity_three = insert(:note_activity)
|
||||
user = insert(:user)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Pleroma.Tests.Helpers
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
test "subdomains_regex/1" do
|
||||
|
@ -59,6 +60,8 @@ test "matches are case-insensitive" do
|
|||
end
|
||||
|
||||
describe "describe/0" do
|
||||
clear_config([:instance, :rewrite_policy])
|
||||
|
||||
test "it works as expected with noop policy" do
|
||||
expected = %{
|
||||
mrf_policies: ["NoOpPolicy"],
|
||||
|
@ -69,7 +72,6 @@ test "it works as expected with noop policy" do
|
|||
end
|
||||
|
||||
test "it works as expected with mock policy" do
|
||||
config = Pleroma.Config.get([:instance, :rewrite_policy])
|
||||
Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock])
|
||||
|
||||
expected = %{
|
||||
|
@ -79,8 +81,6 @@ test "it works as expected with mock policy" do
|
|||
}
|
||||
|
||||
{:ok, ^expected} = MRF.describe()
|
||||
|
||||
Pleroma.Config.put([:instance, :rewrite_policy], config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,12 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do
|
|||
|
||||
alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic
|
||||
|
||||
setup do
|
||||
policy = Pleroma.Config.get([:mrf_rejectnonpublic])
|
||||
on_exit(fn -> Pleroma.Config.put([:mrf_rejectnonpublic], policy) end)
|
||||
|
||||
:ok
|
||||
end
|
||||
clear_config([:mrf_rejectnonpublic])
|
||||
|
||||
describe "public message" do
|
||||
test "it's allowed when address is public" do
|
||||
|
|
|
@ -8,9 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
|
||||
|
||||
setup do
|
||||
orig = Config.get!(:mrf_simple)
|
||||
|
||||
clear_config([:mrf_simple]) do
|
||||
Config.put(:mrf_simple,
|
||||
media_removal: [],
|
||||
media_nsfw: [],
|
||||
|
@ -21,10 +19,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
avatar_removal: [],
|
||||
banner_removal: []
|
||||
)
|
||||
|
||||
on_exit(fn ->
|
||||
Config.put(:mrf_simple, orig)
|
||||
end)
|
||||
end
|
||||
|
||||
describe "when :media_removal" do
|
||||
|
|
|
@ -7,12 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
|
|||
|
||||
alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
|
||||
|
||||
setup do
|
||||
policy = Pleroma.Config.get([:mrf_user_allowlist]) || []
|
||||
on_exit(fn -> Pleroma.Config.put([:mrf_user_allowlist], policy) end)
|
||||
|
||||
:ok
|
||||
end
|
||||
clear_config([:mrf_user_allowlist, :localhost])
|
||||
|
||||
test "pass filter if allow list is empty" do
|
||||
actor = insert(:user)
|
||||
|
|
|
@ -8,8 +8,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
|
|||
alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy
|
||||
|
||||
describe "accept" do
|
||||
clear_config([:mrf_vocabulary, :accept])
|
||||
|
||||
test "it accepts based on parent activity type" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"])
|
||||
|
||||
message = %{
|
||||
|
@ -18,12 +19,9 @@ test "it accepts based on parent activity type" do
|
|||
}
|
||||
|
||||
{:ok, ^message} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
|
||||
end
|
||||
|
||||
test "it accepts based on child object type" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
|
||||
|
||||
message = %{
|
||||
|
@ -35,12 +33,9 @@ test "it accepts based on child object type" do
|
|||
}
|
||||
|
||||
{:ok, ^message} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
|
||||
end
|
||||
|
||||
test "it does not accept disallowed child objects" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
|
||||
|
||||
message = %{
|
||||
|
@ -52,12 +47,9 @@ test "it does not accept disallowed child objects" do
|
|||
}
|
||||
|
||||
{:reject, nil} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
|
||||
end
|
||||
|
||||
test "it does not accept disallowed parent types" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :accept])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"])
|
||||
|
||||
message = %{
|
||||
|
@ -69,14 +61,13 @@ test "it does not accept disallowed parent types" do
|
|||
}
|
||||
|
||||
{:reject, nil} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :accept], config)
|
||||
end
|
||||
end
|
||||
|
||||
describe "reject" do
|
||||
clear_config([:mrf_vocabulary, :reject])
|
||||
|
||||
test "it rejects based on parent activity type" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
|
||||
|
||||
message = %{
|
||||
|
@ -85,12 +76,9 @@ test "it rejects based on parent activity type" do
|
|||
}
|
||||
|
||||
{:reject, nil} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
|
||||
end
|
||||
|
||||
test "it rejects based on child object type" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"])
|
||||
|
||||
message = %{
|
||||
|
@ -102,12 +90,9 @@ test "it rejects based on child object type" do
|
|||
}
|
||||
|
||||
{:reject, nil} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
|
||||
end
|
||||
|
||||
test "it passes through objects that aren't disallowed" do
|
||||
config = Pleroma.Config.get([:mrf_vocabulary, :reject])
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
|
||||
|
||||
message = %{
|
||||
|
@ -116,8 +101,6 @@ test "it passes through objects that aren't disallowed" do
|
|||
}
|
||||
|
||||
{:ok, ^message} = VocabularyPolicy.filter(message)
|
||||
|
||||
Pleroma.Config.put([:mrf_vocabulary, :reject], config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
|
|||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
||||
import Pleroma.Factory
|
||||
import Mock
|
||||
|
||||
test "gets an actor for the relay" do
|
||||
user = Relay.get_actor()
|
||||
|
@ -43,16 +44,21 @@ test "returns activity" do
|
|||
user = insert(:user)
|
||||
service_actor = Relay.get_actor()
|
||||
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 activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
|
||||
assert user.ap_id in activity.recipients
|
||||
assert activity.data["type"] == "Undo"
|
||||
assert activity.data["actor"] == service_actor.ap_id
|
||||
assert activity.data["to"] == [user.ap_id]
|
||||
refute "#{user.ap_id}/followers" in refresh_record(service_actor).following
|
||||
end
|
||||
end
|
||||
|
||||
describe "publish/1" do
|
||||
clear_config([:instance, :federating])
|
||||
|
||||
test "returns error when activity not `Create` type" do
|
||||
activity = insert(:like_activity)
|
||||
assert Relay.publish(activity) == {:error, "Not implemented"}
|
||||
|
@ -63,13 +69,44 @@ test "returns error when activity not public" do
|
|||
assert Relay.publish(activity) == {:error, false}
|
||||
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()
|
||||
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"]
|
||||
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
|
||||
|
|
|
@ -24,6 +24,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
clear_config([:instance, :max_remote_account_fields])
|
||||
|
||||
describe "handle_incoming" do
|
||||
test "it ignores an incoming notice if we already have it" do
|
||||
activity = insert(:note_activity)
|
||||
|
@ -509,6 +511,68 @@ test "it works for incoming update activities" do
|
|||
assert user.bio == "<p>Some bio</p>"
|
||||
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
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
|
||||
|
||||
|
|
|
@ -22,6 +22,21 @@ test "Renders a user, including the public key" do
|
|||
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY")
|
||||
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
|
||||
user = insert(:user)
|
||||
{:ok, user} = User.ensure_keys_present(user)
|
||||
|
|
|
@ -7,6 +7,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
@ -24,6 +26,14 @@ test "Delete" do
|
|||
|> put_req_header("accept", "application/json")
|
||||
|> 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
|
||||
end
|
||||
|
||||
|
@ -35,12 +45,135 @@ test "Create" do
|
|||
|> assign(:user, admin)
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"nickname" => "lain",
|
||||
"email" => "lain@example.org",
|
||||
"password" => "test"
|
||||
"users" => [
|
||||
%{
|
||||
"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
|
||||
|
||||
|
@ -99,6 +232,11 @@ test "allows to force-follow another user" do
|
|||
follower = User.get_cached_by_id(follower.id)
|
||||
|
||||
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
|
||||
|
||||
|
@ -122,6 +260,11 @@ test "allows to force-unfollow another user" do
|
|||
follower = User.get_cached_by_id(follower.id)
|
||||
|
||||
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
|
||||
|
||||
|
@ -142,17 +285,30 @@ test "allows to force-unfollow another user" do
|
|||
}&tags[]=foo&tags[]=bar"
|
||||
)
|
||||
|
||||
%{conn: conn, user1: user1, user2: user2, user3: user3}
|
||||
%{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
|
||||
end
|
||||
|
||||
test "it appends specified tags to users with specified nicknames", %{
|
||||
conn: conn,
|
||||
admin: admin,
|
||||
user1: user1,
|
||||
user2: user2
|
||||
} do
|
||||
assert json_response(conn, :no_content)
|
||||
assert User.get_cached_by_id(user1.id).tags == ["x", "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
|
||||
|
||||
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
|
||||
|
@ -178,17 +334,30 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user
|
|||
}&tags[]=x&tags[]=z"
|
||||
)
|
||||
|
||||
%{conn: conn, user1: user1, user2: user2, user3: user3}
|
||||
%{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
|
||||
end
|
||||
|
||||
test "it removes specified tags from users with specified nicknames", %{
|
||||
conn: conn,
|
||||
admin: admin,
|
||||
user1: user1,
|
||||
user2: user2
|
||||
} do
|
||||
assert json_response(conn, :no_content)
|
||||
assert User.get_cached_by_id(user1.id).tags == []
|
||||
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
|
||||
|
||||
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
|
||||
|
@ -226,6 +395,11 @@ test "/:right POST, can add to a permission group" do
|
|||
assert json_response(conn, 200) == %{
|
||||
"is_admin" => true
|
||||
}
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} made @#{user.nickname} admin"
|
||||
end
|
||||
|
||||
test "/:right DELETE, can remove from a permission group" do
|
||||
|
@ -241,6 +415,11 @@ test "/:right DELETE, can remove from a permission group" do
|
|||
assert json_response(conn, 200) == %{
|
||||
"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
|
||||
|
||||
|
@ -253,10 +432,10 @@ test "/:right DELETE, can remove from a permission group" do
|
|||
|> assign(:user, admin)
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{conn: conn}
|
||||
%{conn: conn, admin: admin}
|
||||
end
|
||||
|
||||
test "deactivates the user", %{conn: conn} do
|
||||
test "deactivates the user", %{conn: conn, admin: admin} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
|
@ -266,9 +445,14 @@ test "deactivates the user", %{conn: conn} do
|
|||
user = User.get_cached_by_id(user.id)
|
||||
assert user.info.deactivated == true
|
||||
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
|
||||
|
||||
test "activates the user", %{conn: conn} do
|
||||
test "activates the user", %{conn: conn, admin: admin} do
|
||||
user = insert(:user, info: %{deactivated: true})
|
||||
|
||||
conn =
|
||||
|
@ -278,6 +462,11 @@ test "activates the user", %{conn: conn} do
|
|||
user = User.get_cached_by_id(user.id)
|
||||
assert user.info.deactivated == false
|
||||
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
|
||||
|
||||
test "returns 403 when requested by a non-admin", %{conn: conn} do
|
||||
|
@ -294,20 +483,17 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do
|
|||
|
||||
describe "POST /api/pleroma/admin/email_invite, with valid config" 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})]
|
||||
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
|
||||
recipient_email = "foo@bar.com"
|
||||
recipient_name = "J. D."
|
||||
|
@ -360,18 +546,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do
|
|||
[user: insert(:user, info: %{is_admin: true})]
|
||||
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
|
||||
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], false)
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
|
||||
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
|
||||
:ok
|
||||
end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|
@ -381,17 +562,9 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u
|
|||
end
|
||||
|
||||
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, :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
|
||||
|> assign(:user, user)
|
||||
|
@ -884,6 +1057,11 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
|
|||
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
|
||||
"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
|
||||
|
||||
describe "GET /api/pleroma/admin/users/invite_token" do
|
||||
|
@ -1069,25 +1247,35 @@ test "returns 404 when report id is invalid", %{conn: conn} do
|
|||
"status_ids" => [activity.id]
|
||||
})
|
||||
|
||||
%{conn: assign(conn, :user, admin), id: report_id}
|
||||
%{conn: assign(conn, :user, admin), id: report_id, admin: admin}
|
||||
end
|
||||
|
||||
test "mark report as resolved", %{conn: conn, id: id} do
|
||||
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|
||||
|> json_response(:ok)
|
||||
|
||||
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
|
||||
|
||||
test "closes report", %{conn: conn, id: id} do
|
||||
test "closes report", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|
||||
|> json_response(:ok)
|
||||
|
||||
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
|
||||
|
||||
test "returns 400 when state is unknown", %{conn: conn, id: id} do
|
||||
|
@ -1218,14 +1406,15 @@ test "returns 403 when requested by anonymous" do
|
|||
end
|
||||
end
|
||||
|
||||
#
|
||||
describe "POST /api/pleroma/admin/reports/:id/respond" do
|
||||
setup %{conn: conn} do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
|
||||
%{conn: assign(conn, :user, admin)}
|
||||
%{conn: assign(conn, :user, admin), admin: admin}
|
||||
end
|
||||
|
||||
test "returns created dm", %{conn: conn} do
|
||||
test "returns created dm", %{conn: conn, admin: admin} do
|
||||
[reporter, target_user] = insert_pair(:user)
|
||||
activity = insert(:note_activity, user: target_user)
|
||||
|
||||
|
@ -1248,6 +1437,13 @@ test "returns created dm", %{conn: conn} do
|
|||
assert reporter.nickname in recipients
|
||||
assert response["content"] == "I will check it out"
|
||||
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
|
||||
|
||||
test "returns 400 when status is missing", %{conn: conn} do
|
||||
|
@ -1271,10 +1467,10 @@ test "returns 404 when report id is invalid", %{conn: conn} do
|
|||
admin = insert(:user, info: %{is_admin: true})
|
||||
activity = insert(:note_activity)
|
||||
|
||||
%{conn: assign(conn, :user, admin), id: activity.id}
|
||||
%{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
|
||||
end
|
||||
|
||||
test "toggle sensitive flag", %{conn: conn, id: id} do
|
||||
test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|
||||
|
@ -1282,6 +1478,11 @@ test "toggle sensitive flag", %{conn: conn, id: id} do
|
|||
|
||||
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 =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|
||||
|
@ -1290,7 +1491,7 @@ test "toggle sensitive flag", %{conn: conn, id: id} do
|
|||
refute response["sensitive"]
|
||||
end
|
||||
|
||||
test "change visibility flag", %{conn: conn, id: id} do
|
||||
test "change visibility flag", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|
||||
|
@ -1298,6 +1499,11 @@ test "change visibility flag", %{conn: conn, id: id} do
|
|||
|
||||
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 =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
|
||||
|
@ -1327,15 +1533,20 @@ test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
|
|||
admin = insert(:user, info: %{is_admin: true})
|
||||
activity = insert(:note_activity)
|
||||
|
||||
%{conn: assign(conn, :user, admin), id: activity.id}
|
||||
%{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
|
||||
end
|
||||
|
||||
test "deletes status", %{conn: conn, id: id} do
|
||||
test "deletes status", %{conn: conn, id: id, admin: admin} do
|
||||
conn
|
||||
|> delete("/api/pleroma/admin/statuses/#{id}")
|
||||
|> json_response(:ok)
|
||||
|
||||
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
|
||||
|
||||
test "returns error when status is not exist", %{conn: conn} do
|
||||
|
@ -1402,17 +1613,13 @@ test "with settings in db", %{conn: conn} do
|
|||
:ok = File.rm(temp_file)
|
||||
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)}
|
||||
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
|
||||
conn =
|
||||
post(conn, "/api/pleroma/admin/config", %{
|
||||
|
@ -1961,17 +2168,13 @@ test "delete part of settings by atom subkeys", %{conn: conn} do
|
|||
:ok = File.rm(temp_file)
|
||||
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}
|
||||
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
|
||||
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
|
||||
conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
|
||||
|
@ -2044,6 +2247,108 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do
|
|||
assert json_response(conn, 200) |> length() == 5
|
||||
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
|
||||
|
||||
# Needed for testing
|
||||
|
|
|
@ -5,18 +5,66 @@
|
|||
defmodule Pleroma.Web.CommonAPITest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
clear_config([:instance, :safe_dm_mentions])
|
||||
clear_config([:instance, :limit])
|
||||
clear_config([:instance, :max_pinned_statuses])
|
||||
|
||||
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
|
||||
|
||||
[participation] = Participation.for_user(user)
|
||||
|
||||
{:ok, convo_reply} =
|
||||
CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id})
|
||||
|
||||
assert Visibility.is_direct?(convo_reply)
|
||||
|
||||
assert activity.data["context"] == convo_reply.data["context"]
|
||||
end
|
||||
|
||||
test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
|
||||
har = insert(:user)
|
||||
jafnhar = insert(:user)
|
||||
tridi = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(har, %{
|
||||
"status" => "@#{jafnhar.nickname} hey",
|
||||
"visibility" => "direct"
|
||||
})
|
||||
|
||||
assert har.ap_id in activity.recipients
|
||||
assert jafnhar.ap_id in activity.recipients
|
||||
|
||||
[participation] = Participation.for_user(har)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(har, %{
|
||||
"status" => "I don't really like @#{tridi.nickname}",
|
||||
"visibility" => "direct",
|
||||
"in_reply_to_status_id" => activity.id,
|
||||
"in_reply_to_conversation_id" => participation.id
|
||||
})
|
||||
|
||||
assert har.ap_id in activity.recipients
|
||||
assert jafnhar.ap_id in activity.recipients
|
||||
refute tridi.ap_id in activity.recipients
|
||||
end
|
||||
|
||||
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
|
||||
har = insert(:user)
|
||||
jafnhar = insert(:user)
|
||||
tridi = insert(:user)
|
||||
option = Pleroma.Config.get([:instance, :safe_dm_mentions])
|
||||
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
|
||||
|
||||
{:ok, activity} =
|
||||
|
@ -27,7 +75,6 @@ test "with the safe_dm_mention option set, it does not mention people beyond the
|
|||
|
||||
refute tridi.ap_id in activity.recipients
|
||||
assert jafnhar.ap_id in activity.recipients
|
||||
Pleroma.Config.put([:instance, :safe_dm_mentions], option)
|
||||
end
|
||||
|
||||
test "it de-duplicates tags" do
|
||||
|
@ -150,15 +197,27 @@ test "it returns error when status is empty and no attachments" do
|
|||
end
|
||||
|
||||
test "it returns error when character limit is exceeded" do
|
||||
limit = Pleroma.Config.get([:instance, :limit])
|
||||
Pleroma.Config.put([:instance, :limit], 5)
|
||||
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, "The status is over the character limit"} =
|
||||
CommonAPI.post(user, %{"status" => "foobar"})
|
||||
end
|
||||
|
||||
Pleroma.Config.put([:instance, :limit], limit)
|
||||
test "it can handle activities that expire" do
|
||||
user = insert(:user)
|
||||
|
||||
expires_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|> NaiveDateTime.add(1_000_000, :second)
|
||||
|
||||
assert {:ok, activity} =
|
||||
CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000})
|
||||
|
||||
assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id)
|
||||
assert expiration.scheduled_at == expires_at
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ test "for public posts, not a reply" do
|
|||
mentioned_user = insert(:user)
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil)
|
||||
|
||||
assert length(to) == 2
|
||||
assert length(cc) == 1
|
||||
|
@ -256,7 +256,7 @@ test "for public posts, a reply" do
|
|||
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil)
|
||||
|
||||
assert length(to) == 3
|
||||
assert length(cc) == 1
|
||||
|
@ -272,7 +272,7 @@ test "for unlisted posts, not a reply" do
|
|||
mentioned_user = insert(:user)
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil)
|
||||
|
||||
assert length(to) == 2
|
||||
assert length(cc) == 1
|
||||
|
@ -289,7 +289,7 @@ test "for unlisted posts, a reply" do
|
|||
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil)
|
||||
|
||||
assert length(to) == 3
|
||||
assert length(cc) == 1
|
||||
|
@ -305,7 +305,7 @@ test "for private posts, not a reply" do
|
|||
mentioned_user = insert(:user)
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil)
|
||||
assert length(to) == 2
|
||||
assert length(cc) == 0
|
||||
|
||||
|
@ -320,7 +320,7 @@ test "for private posts, a reply" do
|
|||
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil)
|
||||
|
||||
assert length(to) == 3
|
||||
assert length(cc) == 0
|
||||
|
@ -335,7 +335,7 @@ test "for direct posts, not a reply" do
|
|||
mentioned_user = insert(:user)
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil)
|
||||
|
||||
assert length(to) == 1
|
||||
assert length(cc) == 0
|
||||
|
@ -350,7 +350,7 @@ test "for direct posts, a reply" do
|
|||
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
|
||||
mentions = [mentioned_user.ap_id]
|
||||
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct")
|
||||
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil)
|
||||
|
||||
assert length(to) == 2
|
||||
assert length(cc) == 0
|
||||
|
|
31
test/web/digest_email_worker_test.exs
Normal file
31
test/web/digest_email_worker_test.exs
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.DigestEmailWorkerTest do
|
||||
use Pleroma.DataCase
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.DigestEmailWorker
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
test "it sends digest emails" do
|
||||
user = insert(:user)
|
||||
|
||||
date =
|
||||
Timex.now()
|
||||
|> Timex.shift(days: -10)
|
||||
|> Timex.to_naive_datetime()
|
||||
|
||||
user2 = insert(:user, last_digest_emailed_at: date)
|
||||
User.switch_email_notifications(user2, "digest", true)
|
||||
CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"})
|
||||
|
||||
DigestEmailWorker.perform()
|
||||
|
||||
assert_received {:email, email}
|
||||
assert email.to == [{user2.name, user2.email}]
|
||||
assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}"
|
||||
end
|
||||
end
|
|
@ -13,15 +13,17 @@ defmodule Pleroma.Web.FederatorTest do
|
|||
setup_all do
|
||||
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
|
||||
end
|
||||
|
||||
clear_config_all([:instance, :federating]) do
|
||||
Pleroma.Config.put([:instance, :federating], true)
|
||||
end
|
||||
|
||||
clear_config([:instance, :allow_relay])
|
||||
clear_config([:instance, :rewrite_policy])
|
||||
clear_config([:mrf_keyword])
|
||||
|
||||
describe "Publisher.perform" do
|
||||
test "call `perform` with unknown task" do
|
||||
assert {
|
||||
|
@ -67,8 +69,6 @@ test "with relays deactivated, it does not publish to the relay", %{
|
|||
end
|
||||
|
||||
refute_received :relay_publish
|
||||
|
||||
Pleroma.Config.put([:instance, :allow_relay], true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -231,19 +231,18 @@ test "rejects incoming AP docs with incorrect origin" do
|
|||
end
|
||||
|
||||
test "it does not crash if MRF rejects the post" do
|
||||
policies = Pleroma.Config.get([:instance, :rewrite_policy])
|
||||
mrf_keyword_policy = Pleroma.Config.get(:mrf_keyword)
|
||||
Pleroma.Config.put([:mrf_keyword, :reject], ["lain"])
|
||||
Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.KeywordPolicy)
|
||||
|
||||
Pleroma.Config.put(
|
||||
[:instance, :rewrite_policy],
|
||||
Pleroma.Web.ActivityPub.MRF.KeywordPolicy
|
||||
)
|
||||
|
||||
params =
|
||||
File.read!("test/fixtures/mastodon-post-activity.json")
|
||||
|> Poison.decode!()
|
||||
|
||||
assert Federator.incoming_ap_doc(params) == :error
|
||||
|
||||
Pleroma.Config.put([:instance, :rewrite_policy], policies)
|
||||
Pleroma.Config.put(:mrf_keyword, mrf_keyword_policy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,14 +10,8 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
config_path = [:instance, :federation_reachability_timeout_days]
|
||||
initial_setting = Pleroma.Config.get(config_path)
|
||||
|
||||
Pleroma.Config.put(config_path, 1)
|
||||
on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
|
||||
|
||||
:ok
|
||||
clear_config_all([:instance, :federation_reachability_timeout_days]) do
|
||||
Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1)
|
||||
end
|
||||
|
||||
describe "set_reachable/1" do
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue